Astro CLI support (#506)
* Astro framework CLI support * Changeset: Added Astro automatic installation * Fixed the package name – was remix, now astro * Fixed the export of the example * Added IPv6 localhost to Astro hostnames * Updated Astro onboarding to show the CLI init command, instead of manual instructions * Astro quickstart * Fix for type in Remix quickstart * Next.js framework detection allows different config file extensions and “next” devDependency * Made the dev command port more general so it works with various frameworks
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Added Astro automatic installation
|
||||
@@ -106,7 +106,7 @@ export function TriggerDevStep() {
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
<Paragraph spacing variant="small">
|
||||
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{" "}
|
||||
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
|
||||
+11
-21
@@ -39,6 +39,8 @@ export default function SetUpAstro() {
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
@@ -76,28 +78,16 @@ export default function SetUpAstro() {
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Follow the steps from the Astro manual installation guide"
|
||||
title="Run the CLI 'init' command in an existing Astro project"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/astro"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
<div className="flex items-start justify-start gap-2"></div>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple
|
||||
example Job in <InlineCode variant="extra-small">example.ts</InlineCode> to help you
|
||||
get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Astro app" />
|
||||
<StepContentContainer>
|
||||
|
||||
@@ -4,4 +4,81 @@ sidebarTitle: "Astro"
|
||||
description: "Start creating Jobs in 5 minutes in your Astro project."
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-astro.mdx" />
|
||||
This quick start guide will get you up and running with Trigger.dev.
|
||||
|
||||
<Accordion title="Need to create a new Astro project to add Trigger.dev to?">
|
||||
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
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Steps titleSize="h3">
|
||||
<Snippet file="quickstart-setup-steps.mdx" />
|
||||
|
||||
<Step title="Run the CLI `dev` command">
|
||||
|
||||
<Snippet file="quickstart-cli-dev.mdx" />
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Advanced: Run your Astro server together with the CLI">
|
||||
You can modify your `package.json` to run both the Astro server and the CLI `dev` command together.
|
||||
|
||||
1. Install the `concurrently` package:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add concurrently --dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
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",
|
||||
//...
|
||||
}
|
||||
//...
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Your first job">
|
||||
|
||||
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:
|
||||
|
||||
<Snippet file="quickstart-example-job.mdx" />
|
||||
|
||||
If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section:
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Snippet file="quickstart-running-your-job.mdx" />
|
||||
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
|
||||
@@ -67,7 +67,7 @@ yarn add concurrently --dev
|
||||
|
||||
<Step title="Your first job">
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<boolean> {
|
||||
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<InstallPackage[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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)/;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
@@ -84,7 +91,10 @@ export async function detectNextDependency(path: string): Promise<boolean> {
|
||||
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<boolean> {
|
||||
|
||||
@@ -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" } }),
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
// export all your job files here
|
||||
|
||||
export * from "./example";
|
||||
@@ -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,
|
||||
});
|
||||
@@ -20,6 +20,20 @@ export async function pathExists(path: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function someFileExists(directory: string, filenames: string[]): Promise<boolean> {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user