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:
Matt Aitken
2023-09-26 03:44:35 -07:00
committed by GitHub
parent 8b25e57613
commit 4578f6bd64
15 changed files with 400 additions and 29 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Added Astro automatic installation
+1 -1
View File
@@ -106,7 +106,7 @@ export function TriggerDevStep() {
</Paragraph>
<TriggerDevCommand />
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
If youre 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">
@@ -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">
Youll notice a new folder in your project called 'jobs'. Weve 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>
+78 -1
View File
@@ -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:
![Your first Job](/images/first-job.png)
</Step>
<Snippet file="quickstart-running-your-job.mdx" />
</Steps>
<Snippet file="quickstart-whats-next.mdx" />
+1 -1
View File
@@ -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);
});
});
+128
View File
@@ -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)/;
}
+2 -1
View File
@@ -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,
+14 -4
View File
@@ -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,
});
+14
View File
@@ -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);
}