Skip to content

Commit fb8153b

Browse files
atinuxvercel[bot]danielroepranaygpadriandlam
authored
feat: add Nuxt module and documentation (#187)
* feat: add Nuxt module and documentation * fix: add the prepare in the build command * Apply suggestion from @vercel[bot] Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> * chore: simplify usage without /nuxt * wip * Update nuxt.mdx Co-Authored-By: Daniel Roe <daniel@roe.dev> * add clean command and ignore prepare * use @workflow/nitro * Changeset Added a Nuxt module and updated documentation accordingly. * feat: enable typescript plugin in tsconfig * chore: revert accordion value * chore: use symlink * fix: json parsing in trigger route for nitro-v2 * ci: add e2e tests for nuxt * ci: add nuxt to test matrix * chore: make sure to add /nuxt to avoid conflicts when importing entry-points * chore: move typescriptPlugin option to Nitro * chore: body is already parsed * fix: use readRawBody * fix: use rawBody in hook.post too * chore: add missing import (but not required) * chore: update pm * Create resolve-symlinks.sh * chore: add also node-rs/xxhash * Update create-test-matrix.mjs * update matrix * Update create-test-matrix.mjs * remove symlink to nitro-v2 --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Adrian Lam <me@adriandlam.com>
1 parent adf0cfe commit fb8153b

File tree

33 files changed

+2303
-291
lines changed

33 files changed

+2303
-291
lines changed

.changeset/tame-doors-melt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@workflow/nuxt": patch
3+
"workflow": patch
4+
---
5+
6+
Add Nuxt module

.github/workflows/tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ jobs:
6767
project-id: "prj_e7DZirYdLrQKXNrlxg7KmA6ABx8r"
6868
- name: "vite"
6969
project-id: "prj_uLIcNZNDmETulAvj5h0IcDHi5432"
70+
- name: "nuxt"
71+
project-id: "prj_oTgiz3SGX2fpZuM6E0P38Ts8de6d"
7072
- name: "sveltekit"
7173
project-id: "prj_MqnBLm71ceXGSnm3Fs8i8gBnI23G"
7274
env:

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ node_modules
33
/packages/*/dist
44
.vercel
55
.turbo
6+
.nuxt
67

78
# This is a hack to get around the fact that `pnpm install`
89
# does not run create the "bin" symlink if the file does not exist
@@ -29,4 +30,4 @@ target
2930

3031
.next
3132

32-
.DS_Store
33+
.DS_Store

docs/app/(home)/components/frameworks.tsx

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -521,15 +521,22 @@ export const Frameworks = () => {
521521
<Next className="size-[56px] relative" />
522522
<Next className="size-[64px] absolute top-0 opacity-15 blur-lg -z-10" />
523523
</Link>
524+
<Link href="/docs/getting-started/hono" className="relative">
525+
<Hono className="size-[56px]" />
526+
<Hono className="size-[64px] absolute top-0 opacity-15 blur-lg -z-10" />
527+
</Link>
524528
<Link href="/docs/getting-started/nitro" className="relative">
525529
<Nitro className="size-[56px]" />
526530
<Nitro className="size-[64px] absolute top-0 opacity-15 blur-lg -z-10" />
527531
</Link>
528-
529532
<Link href="/docs/getting-started/sveltekit" className="relative">
530533
<SvelteKit className="size-[56px]" />
531534
<SvelteKit className="size-[64px] absolute top-0 opacity-15 blur-lg -z-10" />
532535
</Link>
536+
<Link href="/docs/getting-started/nuxt">
537+
<Nuxt className="size-[56px]" />
538+
<Nuxt className="size-[64px] absolute top-0 opacity-15 blur-lg -z-10" />
539+
</Link>
533540

534541
<div className="col-span-4 w-full pt-6">
535542
<div className="flex flex-row items-center gap-2">
@@ -554,20 +561,6 @@ export const Frameworks = () => {
554561
<NestGray className="size-[48px] opacity-70 transition-all duration-200 group-hover:opacity-0 group-hover:scale-95" />
555562
<Nest className="size-[48px] absolute inset-0 opacity-0 scale-95 transition-all duration-200 group-hover:opacity-100 group-hover:scale-100" />
556563
</div>
557-
<div
558-
className="group relative cursor-pointer size-[60px]"
559-
onClick={() => handleRequest('Nuxt')}
560-
>
561-
<NuxtGray className="size-[60px] opacity-70 transition-all duration-200 group-hover:opacity-0 group-hover:scale-95" />
562-
<Nuxt className="size-[60px] absolute inset-0 opacity-0 scale-95 transition-all duration-200 group-hover:opacity-100 group-hover:scale-100" />
563-
</div>
564-
<div
565-
className="group relative cursor-pointer size-[48px]"
566-
onClick={() => handleRequest('Hono')}
567-
>
568-
<HonoGray className="size-[48px] opacity-70 transition-all duration-200 group-hover:opacity-0 group-hover:scale-95" />
569-
<Hono className="size-[48px] absolute inset-0 opacity-0 scale-95 transition-all duration-200 group-hover:opacity-100 group-hover:scale-100" />
570-
</div>
571564
<div
572565
className="group relative cursor-pointer size-[52px]"
573566
onClick={() => handleRequest('Bun')}

docs/content/docs/getting-started/index.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ Start by choosing your framework. Each guide will walk you through the steps to
2525
<SvelteKit className="size-16" />
2626
<span className="font-medium">SvelteKit</span>
2727
</Card>
28-
<Card href="/docs/getting-started/nuxt" disabled className="flex flex-col items-center justify-center text-center gap-2">
28+
<Card href="/docs/getting-started/nuxt" className="flex flex-col items-center justify-center text-center gap-2">
2929
<Nuxt className="size-16" />
3030
<span className="font-medium">Nuxt</span>
31-
<Badge variant="secondary">Coming soon</Badge>
3231
</Card>
3332
</Cards>
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
title: Nuxt
3+
---
4+
5+
# Nuxt
6+
7+
This guide will walk through setting up your first workflow in a Nuxt app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects.
8+
9+
---
10+
11+
<Steps>
12+
13+
<Step>
14+
## Create Your Nuxt Project
15+
16+
Start by creating a new Nuxt project. This command will create a new directory named `nuxt-app` and setup a Nuxt project inside it.
17+
18+
```bash
19+
npm create nuxt@latest nuxt-app
20+
```
21+
22+
Enter the newly made directory:
23+
24+
```bash
25+
cd nuxt-app
26+
```
27+
28+
### Install `workflow`
29+
30+
<Tabs items={['npm', 'pnpm', 'yarn', 'bun']}>
31+
<Tab value="npm">
32+
<CodeBlock>npm i workflow</CodeBlock>
33+
</Tab>
34+
<Tab value="pnpm">
35+
<CodeBlock>pnpm i workflow</CodeBlock>
36+
</Tab>
37+
<Tab value="yarn">
38+
<CodeBlock>yarn add workflow</CodeBlock>
39+
</Tab>
40+
<Tab value="bun">
41+
<CodeBlock>bun add workflow</CodeBlock>
42+
</Tab>
43+
</Tabs>
44+
45+
### Configure Nuxt
46+
47+
Add `workflow` to your `nuxt.config.ts`. This automatically configures the Nitro integration and enables usage of the `"use workflow"` and `"use step"` directives.
48+
49+
```typescript title="nuxt.config.ts" lineNumbers
50+
import { defineNuxtConfig } from "nuxt/config";
51+
52+
export default defineNuxtConfig({
53+
modules: ["workflow/nuxt"], // [!code highlight]
54+
compatibilityDate: "latest",
55+
});
56+
```
57+
58+
This will also automatically enable the TypeScript plugin, which provides helpful IntelliSense hints in your IDE for workflow and step functions.
59+
60+
<Accordion type="single" collapsible>
61+
<AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
62+
<AccordionTrigger className="[&_p]:my-0 text-lg [&_p]:text-foreground">
63+
Disable TypeScript Plugin (Optional)
64+
</AccordionTrigger>
65+
<AccordionContent className="[&_p]:my-2">
66+
The TypeScript plugin is enabled by default. If you need to disable it, you can configure it in your `nuxt.config.ts`:
67+
68+
```typescript title="nuxt.config.ts" lineNumbers
69+
export default defineNuxtConfig({
70+
modules: ["workflow/nuxt"],
71+
workflow: {
72+
typescriptPlugin: false, // [!code highlight]
73+
},
74+
compatibilityDate: "latest",
75+
});
76+
```
77+
78+
</AccordionContent>
79+
80+
</AccordionItem>
81+
</Accordion>
82+
83+
</Step>
84+
85+
<Step>
86+
87+
## Create Your First Workflow
88+
89+
Create a new file for our first workflow:
90+
91+
```typescript title="server/workflows/user-signup.ts" lineNumbers
92+
import { sleep } from "workflow";
93+
94+
export async function handleUserSignup(email: string) {
95+
"use workflow"; // [!code highlight]
96+
97+
const user = await createUser(email);
98+
await sendWelcomeEmail(user);
99+
100+
await sleep("5s"); // Pause for 5s - doesn't consume any resources
101+
await sendOnboardingEmail(user);
102+
103+
return { userId: user.id, status: "onboarded" };
104+
}
105+
```
106+
107+
We'll fill in those functions next, but let's take a look at this code:
108+
109+
- We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**.
110+
- The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long.
111+
112+
## Create Your Workflow Steps
113+
114+
Let's now define those missing functions.
115+
116+
```typescript title="server/workflows/user-signup.ts" lineNumbers
117+
import { FatalError } from "workflow";
118+
119+
// Our workflow function defined earlier
120+
121+
async function createUser(email: string) {
122+
"use step"; // [!code highlight]
123+
124+
console.log(`Creating user with email: ${email}`);
125+
126+
// Full Node.js access - database calls, APIs, etc.
127+
return { id: crypto.randomUUID(), email };
128+
}
129+
130+
async function sendWelcomeEmail(user: { id: string; email: string }) {
131+
"use step"; // [!code highlight]
132+
133+
console.log(`Sending welcome email to user: ${user.id}`);
134+
135+
if (Math.random() < 0.3) {
136+
// By default, steps will be retried for unhandled errors
137+
throw new Error("Retryable!");
138+
}
139+
}
140+
141+
async function sendOnboardingEmail(user: { id: string; email: string }) {
142+
"use step"; // [!code highlight]
143+
144+
if (!user.email.includes("@")) {
145+
// To skip retrying, throw a FatalError instead
146+
throw new FatalError("Invalid Email");
147+
}
148+
149+
console.log(`Sending onboarding email to user: ${user.id}`);
150+
}
151+
```
152+
153+
Taking a look at this code:
154+
155+
- Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`.
156+
- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
157+
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
158+
159+
<Callout>
160+
We'll dive deeper into workflows, steps, and other ways to suspend or handle
161+
events in [Foundations](/docs/foundations).
162+
</Callout>
163+
164+
</Step>
165+
166+
<Step>
167+
168+
## Create Your API Route
169+
170+
To invoke your new workflow, we'll create a new API route handler at `server/api/signup.post.ts` with the following code:
171+
172+
```typescript title="server/api/signup.post.ts"
173+
import { start } from 'workflow/api';
174+
import { defineEventHandler, readBody } from 'h3';
175+
import { handleUserSignup } from "../workflows/user-signup";
176+
177+
export default defineEventHandler(async (event) => {
178+
const { email } = await readBody(event);
179+
180+
// Executes asynchronously and doesn't block your app
181+
await start(handleUserSignup, [email]);
182+
183+
return {
184+
message: "User signup workflow started",
185+
};
186+
});
187+
```
188+
189+
This API route creates a `POST` request endpoint at `/api/signup` that will trigger your workflow.
190+
191+
<Callout>
192+
Workflows can be triggered from API routes or any server-side
193+
code.
194+
</Callout>
195+
196+
</Step>
197+
198+
<Step>
199+
200+
## Run in development
201+
202+
To start your development server, run the following command in your terminal in the Nuxt root directory:
203+
204+
```bash
205+
npm run dev
206+
```
207+
208+
Once your development server is running, you can trigger your workflow by running this command in the terminal:
209+
210+
```bash
211+
curl -X POST --json '{"email":"hello@example.com"}' http://localhost:3000/api/signup
212+
```
213+
214+
Check the Nuxt development server logs to see your workflow execute as well as the steps that are being processed.
215+
216+
Additionally, you can use the [Workflow DevKit CLI or Web UI](/docs/observability) to inspect your workflow runs and steps in detail.
217+
218+
```bash
219+
npx workflow inspect runs # add '--web' for an interactive Web based UI
220+
```
221+
222+
<img src="/o11y-ui.png" alt="Workflow DevKit Web UI" />
223+
224+
</Step>
225+
226+
</Steps>
227+
228+
---
229+
230+
## Deploying to production
231+
232+
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
233+
234+
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
235+
236+
## Next Steps
237+
238+
- Learn more about the [Foundations](/docs/foundations).
239+
- Check [Errors](/docs/errors) if you encounter issues.
240+
- Explore the [API Reference](/docs/api-reference).
241+

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"engines": {
1717
"node": "^18.0.0 || ^20.0.0 || ^22.0.0 || ^24.0.0"
1818
},
19-
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
19+
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
2020
"pnpm": {
2121
"overrides": {
2222
"rfc6902": "5.1.2"

packages/core/e2e/local-build.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe.each([
1111
'nitro',
1212
'vite',
1313
'sveltekit',
14+
'nuxt',
1415
])('e2e', (project) => {
1516
test('builds without errors', { timeout: 180_000 }, async () => {
1617
// skip if we're targeting specific app to test

packages/nitro/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ export default {
2222
nitro.options.alias['debug'] ??= 'debug';
2323
}
2424

25+
// Add tsConfig plugin
26+
if (nitro.options.workflow?.typescriptPlugin) {
27+
nitro.options.typescript.tsConfig ||= {};
28+
nitro.options.typescript.tsConfig.compilerOptions ||= {};
29+
nitro.options.typescript.tsConfig.compilerOptions.plugins ||= [];
30+
nitro.options.typescript.tsConfig.compilerOptions.plugins.push({
31+
name: 'workflow',
32+
});
33+
}
34+
2535
// Generate functions for vercel build
2636
if (isVercelDeploy) {
2737
nitro.hooks.hook('compiled', async () => {

packages/nitro/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export interface ModuleOptions {
88
* By default, `workflows/` directory will be scanned from root and all layer source dirs.
99
*/
1010
dirs?: string[];
11+
12+
/**
13+
* Enable workflow TypeScript plugin in generated tsconfig.json
14+
* @default false
15+
*/
16+
typescriptPlugin?: boolean;
1117
}
1218

1319
declare module 'nitro/types' {

0 commit comments

Comments
 (0)