diff --git a/.changeset/perfect-poems-reply.md b/.changeset/perfect-poems-reply.md new file mode 100644 index 0000000000..881866dee0 --- /dev/null +++ b/.changeset/perfect-poems-reply.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +Added Astro automatic installation diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index 2f501ea9a1..245eeff8ba 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -106,7 +106,7 @@ export function TriggerDevStep() { - If you’re not running on port 3000 you can specify the port by adding{" "} + If you’re not running on the default you can specify the port by adding{" "} --port 3001 to the end. diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx index 10bd0727c3..61ce6d0e3e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.astro/route.tsx @@ -39,6 +39,8 @@ export default function SetUpAstro() { useProjectSetupComplete(); const devEnvironment = useDevEnvironment(); invariant(devEnvironment, "Dev environment must be defined"); + const appOrigin = useAppOrigin(); + return (
@@ -76,28 +78,16 @@ export default function SetUpAstro() {
- - Copy your server API Key to your clipboard: -
- Server} - /> -
- Now follow this guide: - - Manual installation guide - -
+ + + + + You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple + example Job in example.ts to help you + get started. + diff --git a/docs/documentation/quickstarts/astro.mdx b/docs/documentation/quickstarts/astro.mdx index f9b9968caf..f1d0a92982 100644 --- a/docs/documentation/quickstarts/astro.mdx +++ b/docs/documentation/quickstarts/astro.mdx @@ -4,4 +4,81 @@ sidebarTitle: "Astro" description: "Start creating Jobs in 5 minutes in your Astro project." --- - +This quick start guide will get you up and running with Trigger.dev. + + +No problem, create a blank project by running the `create-astro` command in your terminal then continue with this quickstart guide as normal: + +```bash +npx create-astro@latest +``` + + + + + + + + + + + + +You can modify your `package.json` to run both the Astro server and the CLI `dev` command together. + +1. Install the `concurrently` package: + + + +```bash npm +npm install concurrently --save-dev +``` + +```bash pnpm +pnpm install concurrently --save-dev +``` + +```bash yarn +yarn add concurrently --dev +``` + + + +2. Modify your `package.json` file's `dev` script. + +```json package.json +//... +"scripts": { + "dev": "concurrently --kill-others npm:dev:*", + //your normal astro dev command would go here + "dev:astro": "astro dev", + "dev:trigger": "npx @trigger.dev/cli dev", + //... +} +//... +``` + + + + + + + + +The CLI init command created a simple Job for you. There will be a new file `src/jobs/example.(ts/js)`. + +In there is this Job: + + + +If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section: + +![Your first Job](/images/first-job.png) + + + + + + + + diff --git a/docs/documentation/quickstarts/remix.mdx b/docs/documentation/quickstarts/remix.mdx index 2d8cc21604..4af18372ea 100644 --- a/docs/documentation/quickstarts/remix.mdx +++ b/docs/documentation/quickstarts/remix.mdx @@ -67,7 +67,7 @@ yarn add concurrently --dev -The CLI init command created a simple Job for you. There will be a new file either `app/jobs/example.server.(ts/js)`. +The CLI init command created a simple Job for you. There will be a new file `app/jobs/example.server.(ts/js)`. In there is this Job: diff --git a/packages/cli/src/frameworks/astro/astro.test.ts b/packages/cli/src/frameworks/astro/astro.test.ts new file mode 100644 index 0000000000..57bd3cf783 --- /dev/null +++ b/packages/cli/src/frameworks/astro/astro.test.ts @@ -0,0 +1,81 @@ +import mock from "mock-fs"; +import { Astro } from "."; +import { getFramework } from ".."; +import { pathExists } from "../../utils/fileSystem"; + +afterEach(() => { + mock.restore(); +}); + +describe("Astro project detection", () => { + test("has dependency", async () => { + mock({ + "package.json": JSON.stringify({ dependencies: { astro: "1.0.0" } }), + }); + + const framework = await getFramework("", "npm"); + expect(framework?.id).toEqual("astro"); + }); + + test("no dependency, has astro.config.js", async () => { + mock({ + "package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }), + "astro.config.js": "module.exports = {}", + }); + + const framework = await getFramework("", "npm"); + expect(framework?.id).toEqual("astro"); + }); + + test("no dependency, has astro.config.mjs", async () => { + mock({ + "package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }), + "astro.config.mjs": "module.exports = {}", + }); + + const framework = await getFramework("", "npm"); + expect(framework?.id).toEqual("astro"); + }); + + test("no dependency, no astro.config.*", async () => { + mock({ + "package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }), + }); + + const framework = await getFramework("", "npm"); + expect(framework?.id).not.toEqual("astro"); + }); +}); + +describe("install", () => { + test("javascript", async () => { + mock({ + src: { + pages: {}, + }, + }); + + const astro = new Astro(); + await astro.install("", { typescript: false, packageManager: "npm", endpointSlug: "foo" }); + expect(await pathExists("src/trigger.js")).toEqual(true); + expect(await pathExists("src/pages/api/trigger.js")).toEqual(true); + expect(await pathExists("src/jobs/example.js")).toEqual(true); + expect(await pathExists("src/jobs/index.js")).toEqual(true); + }); + + test("typescript", async () => { + mock({ + app: { + routes: {}, + }, + "tsconfig.json": JSON.stringify({}), + }); + + const astro = new Astro(); + await astro.install("", { typescript: true, packageManager: "npm", endpointSlug: "foo" }); + expect(await pathExists("src/trigger.ts")).toEqual(true); + expect(await pathExists("src/pages/api/trigger.ts")).toEqual(true); + expect(await pathExists("src/jobs/example.ts")).toEqual(true); + expect(await pathExists("src/jobs/index.ts")).toEqual(true); + }); +}); diff --git a/packages/cli/src/frameworks/astro/index.ts b/packages/cli/src/frameworks/astro/index.ts new file mode 100644 index 0000000000..bdb8ac67cb --- /dev/null +++ b/packages/cli/src/frameworks/astro/index.ts @@ -0,0 +1,128 @@ +import { Framework, ProjectInstallOptions } from ".."; +import { InstallPackage } from "../../utils/addDependencies"; +import { pathExists, someFileExists } from "../../utils/fileSystem"; +import { PackageManager } from "../../utils/getUserPkgManager"; +import pathModule from "path"; +import { getPathAlias } from "../../utils/pathAlias"; +import { createFileFromTemplate } from "../../utils/createFileFromTemplate"; +import { templatesPath } from "../../paths"; +import { logger } from "../../utils/logger"; +import { readPackageJson } from "../../utils/readPackageJson"; +import { standardWatchFilePaths } from "../watchConfig"; + +export class Astro implements Framework { + id = "astro"; + name = "Astro"; + + async isMatch(path: string, packageManager: PackageManager): Promise { + const configFilenames = [ + "astro.config.js", + "astro.config.mjs", + "astro.config.cjs", + "astro.config.ts", + ]; + //check for astro.config.mjs + const hasConfigFile = await someFileExists(path, configFilenames); + if (hasConfigFile) { + return true; + } + + //check for the astro package + const packageJsonContent = await readPackageJson(path); + if (packageJsonContent?.dependencies?.astro) { + return true; + } + + return false; + } + + async dependencies(): Promise { + return [ + { name: "@trigger.dev/sdk", tag: "latest" }, + { name: "@trigger.dev/astro", tag: "latest" }, + { name: "@trigger.dev/react", tag: "latest" }, + ]; + } + + possibleEnvFilenames(): string[] { + return [".env", ".env.development"]; + } + + async install(path: string, { typescript, endpointSlug }: ProjectInstallOptions): Promise { + const pathAlias = await getPathAlias({ + projectPath: path, + isTypescriptProject: typescript, + extraDirectories: ["src"], + }); + const templatesDir = pathModule.join(templatesPath(), "astro"); + const srcFolder = pathModule.join(path, "src"); + const fileExtension = typescript ? ".ts" : ".js"; + + //create src/pages/api/trigger.js + const apiRoutePath = pathModule.join(srcFolder, "pages", "api", `trigger${fileExtension}`); + const apiRouteResult = await createFileFromTemplate({ + templatePath: pathModule.join(templatesDir, "apiRoute.js"), + replacements: { + routePathPrefix: pathAlias ? pathAlias + "/" : "../../", + }, + outputPath: apiRoutePath, + }); + if (!apiRouteResult.success) { + throw new Error("Failed to create API route file"); + } + logger.success(`✔ Created API route at ${apiRoutePath}`); + + //src/trigger.js + const triggerFilePath = pathModule.join(srcFolder, `trigger${fileExtension}`); + const triggerResult = await createFileFromTemplate({ + templatePath: pathModule.join(templatesDir, "trigger.js"), + replacements: { + endpointSlug, + }, + outputPath: triggerFilePath, + }); + if (!triggerResult.success) { + throw new Error("Failed to create trigger file"); + } + logger.success(`✔ Created Trigger client at ${triggerFilePath}`); + + //src/jobs/example.js + const exampleJobFilePath = pathModule.join(srcFolder, "jobs", `example${fileExtension}`); + const exampleJobResult = await createFileFromTemplate({ + templatePath: pathModule.join(templatesDir, "exampleJob.js"), + replacements: { + jobsPathPrefix: pathAlias ? pathAlias + "/" : "../", + }, + outputPath: exampleJobFilePath, + }); + if (!exampleJobResult.success) { + throw new Error("Failed to create example job file"); + } + logger.success(`✔ Created example job at ${exampleJobFilePath}`); + + //src/jobs/index.js + const jobsIndexFilePath = pathModule.join(srcFolder, "jobs", `index${fileExtension}`); + const jobsIndexResult = await createFileFromTemplate({ + templatePath: pathModule.join(templatesDir, "jobsIndex.js"), + replacements: { + jobsPathPrefix: pathAlias ? pathAlias + "/" : "../", + }, + outputPath: jobsIndexFilePath, + }); + if (!jobsIndexResult.success) { + throw new Error("Failed to create jobs index file"); + } + logger.success(`✔ Created jobs index at ${jobsIndexFilePath}`); + } + + async postInstall(path: string, options: ProjectInstallOptions): Promise { + logger.warn( + `⚠︎ Ensure your astro.config output is "server" or "hybrid":\nhttps://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project` + ); + } + + defaultHostnames = ["localhost", "[::]"]; + defaultPorts = [4321, 4322, 4323, 4324]; + watchFilePaths = standardWatchFilePaths; + watchIgnoreRegex = /(node_modules)/; +} diff --git a/packages/cli/src/frameworks/index.ts b/packages/cli/src/frameworks/index.ts index 960791d6ec..69ba77a476 100644 --- a/packages/cli/src/frameworks/index.ts +++ b/packages/cli/src/frameworks/index.ts @@ -1,5 +1,6 @@ import { InstallPackage } from "../utils/addDependencies"; import { PackageManager } from "../utils/getUserPkgManager"; +import { Astro } from "./astro"; import { NextJs } from "./nextjs"; import { Remix } from "./remix"; @@ -45,7 +46,7 @@ export interface Framework { } /** The order of these matters. The first one that matches the folder will be used, so stricter ones should be first. */ -const frameworks: Framework[] = [new NextJs(), new Remix()]; +const frameworks: Framework[] = [new NextJs(), new Remix(), new Astro()]; export const getFramework = async ( path: string, diff --git a/packages/cli/src/frameworks/nextjs/index.ts b/packages/cli/src/frameworks/nextjs/index.ts index 1ec073961d..5d60320737 100644 --- a/packages/cli/src/frameworks/nextjs/index.ts +++ b/packages/cli/src/frameworks/nextjs/index.ts @@ -4,13 +4,13 @@ import { Framework } from ".."; import { templatesPath } from "../../paths"; import { InstallPackage } from "../../utils/addDependencies"; import { createFileFromTemplate } from "../../utils/createFileFromTemplate"; -import { pathExists } from "../../utils/fileSystem"; +import { pathExists, someFileExists } from "../../utils/fileSystem"; import { PackageManager } from "../../utils/getUserPkgManager"; import { logger } from "../../utils/logger"; import { getPathAlias } from "../../utils/pathAlias"; import { readPackageJson } from "../../utils/readPackageJson"; -import { detectMiddlewareUsage } from "./middleware"; import { standardWatchFilePaths } from "../watchConfig"; +import { detectMiddlewareUsage } from "./middleware"; export class NextJs implements Framework { id = "nextjs"; @@ -75,7 +75,14 @@ export class NextJs implements Framework { } async function detectNextConfigFile(path: string): Promise { - return pathExists(pathModule.join(path, "next.config.js")); + const configFilenames = [ + "next.config.js", + "next.config.mjs", + "next.config.cjs", + "next.config.ts", + ]; + + return someFileExists(path, configFilenames); } export async function detectNextDependency(path: string): Promise { @@ -84,7 +91,10 @@ export async function detectNextDependency(path: string): Promise { return false; } - return packageJsonContent.dependencies?.next !== undefined; + if (packageJsonContent.dependencies?.next !== undefined) return true; + if (packageJsonContent.devDependencies?.next !== undefined) return true; + + return false; } export async function detectUseOfSrcDir(path: string): Promise { diff --git a/packages/cli/src/frameworks/nextjs/nextjs.test.ts b/packages/cli/src/frameworks/nextjs/nextjs.test.ts index 63f192eb7d..494a54f273 100644 --- a/packages/cli/src/frameworks/nextjs/nextjs.test.ts +++ b/packages/cli/src/frameworks/nextjs/nextjs.test.ts @@ -17,6 +17,15 @@ describe("Next project detection", () => { expect(framework?.id).toEqual("nextjs"); }); + test("has dev dependency", async () => { + mock({ + "package.json": JSON.stringify({ devDependencies: { next: "1.0.0" } }), + }); + + const framework = await getFramework("", "npm"); + expect(framework?.id).toEqual("nextjs"); + }); + test("no dependency, has next.config.js", async () => { mock({ "package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }), @@ -27,6 +36,16 @@ describe("Next project detection", () => { expect(framework?.id).toEqual("nextjs"); }); + test("no dependency, has next.config.mjs", async () => { + mock({ + "package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }), + "next.config.mjs": "module.exports = {}", + }); + + const framework = await getFramework("", "npm"); + expect(framework?.id).toEqual("nextjs"); + }); + test("no dependency, no next.config.js", async () => { mock({ "package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }), diff --git a/packages/cli/src/templates/astro/apiRoute.js b/packages/cli/src/templates/astro/apiRoute.js new file mode 100644 index 0000000000..502724ef88 --- /dev/null +++ b/packages/cli/src/templates/astro/apiRoute.js @@ -0,0 +1,9 @@ +import { createAstroRoute } from "@trigger.dev/astro"; +//you may need to update this path to point at your trigger.ts file +import { client } from "${routePathPrefix}trigger"; + +//import your jobs +import "${routePathPrefix}jobs"; + +export const prerender = false; +export const { POST } = createAstroRoute(client); diff --git a/packages/cli/src/templates/astro/exampleJob.js b/packages/cli/src/templates/astro/exampleJob.js new file mode 100644 index 0000000000..ff0fbdc800 --- /dev/null +++ b/packages/cli/src/templates/astro/exampleJob.js @@ -0,0 +1,27 @@ +import { eventTrigger } from "@trigger.dev/sdk"; +import { client } from "${jobsPathPrefix}trigger"; + +// Your first job +// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline +client.defineJob({ + // This is the unique identifier for your Job, it must be unique across all Jobs in your project + id: "example-job", + name: "Example Job: a joke with a delay", + version: "0.0.1", + // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction + trigger: eventTrigger({ + name: "example.event", + }), + run: async (payload, io, ctx) => { + // This logs a message to the console + await io.logger.info("🧪 Example Job: a joke with a delay"); + await io.logger.info("How do you comfort a JavaScript bug?"); + // This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year + await io.wait("Wait 5 seconds for the punchline...", 5); + await io.logger.info("You console it! 🤦"); + await io.logger.info( + "✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨" + ); + // To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job + }, +}); diff --git a/packages/cli/src/templates/astro/jobsIndex.js b/packages/cli/src/templates/astro/jobsIndex.js new file mode 100644 index 0000000000..c24223d379 --- /dev/null +++ b/packages/cli/src/templates/astro/jobsIndex.js @@ -0,0 +1,3 @@ +// export all your job files here + +export * from "./example"; diff --git a/packages/cli/src/templates/astro/trigger.js b/packages/cli/src/templates/astro/trigger.js new file mode 100644 index 0000000000..ef08f13b48 --- /dev/null +++ b/packages/cli/src/templates/astro/trigger.js @@ -0,0 +1,7 @@ +import { TriggerClient } from "@trigger.dev/sdk"; + +export const client = new TriggerClient({ + id: "${endpointSlug}", + apiKey: import.meta.env.TRIGGER_API_KEY, + apiUrl: import.meta.env.TRIGGER_API_URL, +}); diff --git a/packages/cli/src/utils/fileSystem.ts b/packages/cli/src/utils/fileSystem.ts index 76c350eb98..f1acf955b0 100644 --- a/packages/cli/src/utils/fileSystem.ts +++ b/packages/cli/src/utils/fileSystem.ts @@ -20,6 +20,20 @@ export async function pathExists(path: string): Promise { } } +export async function someFileExists(directory: string, filenames: string[]): Promise { + for (let index = 0; index < filenames.length; index++) { + const filename = filenames[index]; + if (!filename) continue; + + const path = pathModule.join(directory, filename); + if (await pathExists(path)) { + return true; + } + } + + return false; +} + export async function removeFile(path: string) { await fsModule.unlink(path); }