Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/twenty-socks-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/nitro": patch
---

Bundle workflow routes within the Nitro server using base builder, use Nitro v3 `functionRules` for Vercel workflow routes
21 changes: 12 additions & 9 deletions packages/nitro/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
import type { Nitro } from 'nitro/types';
import { join } from 'pathe';

export class VercelBuilder extends VercelBuildOutputAPIBuilder {
/**
* Nitro v2 Vercel deploy builder. Uses the Build Output API to generate
* Vercel function config and routes, since v2 doesn't support `functionRules`.
*/
export class LegacyVercelBuilder extends VercelBuildOutputAPIBuilder {
constructor(nitro: Nitro) {
super({
...createBaseBuilderConfig({
Expand All @@ -31,7 +35,7 @@ export class VercelBuilder extends VercelBuildOutputAPIBuilder {
}
}

export class LocalBuilder extends BaseBuilder {
export class WorkflowBuilder extends BaseBuilder {
#outDir: string;
constructor(nitro: Nitro) {
const outDir = join(nitro.options.buildDir, 'workflow');
Expand Down Expand Up @@ -64,24 +68,23 @@ export class LocalBuilder extends BaseBuilder {
inputFiles,
});

const webhookRouteFile = join(this.#outDir, 'webhook.mjs');

await this.createWebhookBundle({
outfile: webhookRouteFile,
outfile: join(this.#outDir, 'webhook.mjs'),
bundle: false,
});

// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
workflows: {
...stepsManifest.workflows,
...workflowsManifest.workflows,
},
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

// Generate manifest
const workflowBundlePath = join(this.#outDir, 'workflows.mjs');
await this.createManifest({
workflowBundlePath,
workflowBundlePath: join(this.#outDir, 'workflows.mjs'),
manifestDir: this.#outDir,
manifest,
});
Expand Down
158 changes: 93 additions & 65 deletions packages/nitro/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import {
STEP_QUEUE_TRIGGER,
WORKFLOW_QUEUE_TRIGGER,
} from '@workflow/builders';
import { workflowTransformPlugin } from '@workflow/rollup';
import type { Nitro, NitroModule, RollupConfig } from 'nitro/types';
import { join } from 'pathe';
import { LocalBuilder, VercelBuilder } from './builders.js';
import { LegacyVercelBuilder, WorkflowBuilder } from './builders.js';
import type { ModuleOptions } from './types';

export { WorkflowBuilder } from './builders.js';
export type { ModuleOptions };

export default {
Expand Down Expand Up @@ -34,12 +39,13 @@ export default {
nitro.options.alias['debug'] ??= 'debug';
}

// NOTE: Externalize .nitro/workflow to prevent dev reloads
if (nitro.options.dev) {
nitro.options.externals ||= {};
nitro.options.externals.external ||= [];
// NOTE: Externalize .nitro/workflow to prevent dev reloads (Nitro v2 only)
if (nitro.options.dev && !nitro.routing) {
const opts = nitro.options as Record<string, any>;
opts.externals ||= {};
opts.externals.external ||= [];
const outDir = join(nitro.options.buildDir, 'workflow');
nitro.options.externals.external.push((id) => id.startsWith(outDir));
opts.externals.external.push((id: string) => id.startsWith(outDir));
}

// Add tsConfig plugin
Expand All @@ -52,79 +58,101 @@ export default {
});
}

// Generate functions for vercel build
if (isVercelDeploy) {
// Nitro v2 Vercel deploy: use legacy VercelBuilder approach
if (isVercelDeploy && !nitro.routing) {
nitro.hooks.hook('compiled', async () => {
await new VercelBuilder(nitro).build();
await new LegacyVercelBuilder(nitro).build();
});
return;
}

// Generate local bundles for dev and local prod
if (!isVercelDeploy) {
const builder = new LocalBuilder(nitro);
let isInitialBuild = true;
// Nitro v3+ Vercel deploy: configure function rules for workflow routes (queue triggers, maxDuration).
if (isVercelDeploy) {
nitro.options.vercel ??= {};
nitro.options.vercel.functionRules ??= {};

nitro.hooks.hook('build:before', async () => {
await builder.build();
const runtime = nitro.options.workflow?.runtime;

// For prod: write the manifest handler file with inlined content
// now that the builder has generated the manifest. Rollup will
// bundle this file into the compiled output.
if (
!nitro.options.dev &&
process.env.WORKFLOW_PUBLIC_MANIFEST === '1'
) {
writeManifestHandler(nitro);
}
});
nitro.options.vercel.functionRules['/.well-known/workflow/v1/step'] = {
...(runtime && { runtime }),
// @ts-expect-error - TODO: fixed in next nitro release
maxDuration: 'max',
experimentalTriggers: [STEP_QUEUE_TRIGGER],
};

nitro.options.vercel.functionRules['/.well-known/workflow/v1/flow'] = {
...(runtime && { runtime }),
maxDuration: 60,
experimentalTriggers: [WORKFLOW_QUEUE_TRIGGER],
};

// Allows for HMR - but skip the first dev:reload since build:before already ran
if (nitro.options.dev) {
nitro.hooks.hook('dev:reload', async () => {
if (isInitialBuild) {
isInitialBuild = false;
return;
}
await builder.build();
});
if (runtime) {
nitro.options.vercel.functionRules[
'/.well-known/workflow/v1/webhook/**'
] = { runtime };
}
}

addVirtualHandler(
nitro,
'/.well-known/workflow/v1/webhook/:token',
'workflow/webhook.mjs'
);
// Generate workflow bundles (used by virtual handlers below)
const builder = new WorkflowBuilder(nitro);
let isInitialBuild = true;

addVirtualHandler(
nitro,
'/.well-known/workflow/v1/step',
'workflow/steps.mjs'
);
nitro.hooks.hook('build:before', async () => {
await builder.build();

addVirtualHandler(
nitro,
'/.well-known/workflow/v1/flow',
'workflow/workflows.mjs'
);
// For prod: write the manifest handler file with inlined content
// now that the builder has generated the manifest. Rollup will
// bundle this file into the compiled output.
if (!nitro.options.dev && process.env.WORKFLOW_PUBLIC_MANIFEST === '1') {
writeManifestHandler(nitro);
}
});

// Expose manifest as a public HTTP route when WORKFLOW_PUBLIC_MANIFEST=1
if (process.env.WORKFLOW_PUBLIC_MANIFEST === '1') {
// Write a placeholder manifest-data.mjs so rollup can resolve the
// import. It will be overwritten with the real manifest in build:before.
// Write a placeholder handler file so rollup can resolve the path
// during prod compilation. It will be overwritten with the real
// manifest content by writeManifestHandler() in build:before.
if (!nitro.options.dev) {
const dir = join(nitro.options.buildDir, 'workflow');
mkdirSync(dir, { recursive: true });
const handlerPath = join(dir, 'manifest-handler.mjs');
writeFileSync(
handlerPath,
'export default async () => new Response("Manifest not found", { status: 404 });\n'
);
// Allows for HMR - but skip the first dev:reload since build:before already ran
if (nitro.options.dev) {
nitro.hooks.hook('dev:reload', async () => {
if (isInitialBuild) {
isInitialBuild = false;
return;
}
addManifestHandler(nitro);
await builder.build();
});
}

// Register workflow routes as handlers
addVirtualHandler(
nitro,
'/.well-known/workflow/v1/webhook/:token',
'workflow/webhook.mjs'
);

addVirtualHandler(
nitro,
'/.well-known/workflow/v1/step',
'workflow/steps.mjs'
);

addVirtualHandler(
nitro,
'/.well-known/workflow/v1/flow',
'workflow/workflows.mjs'
);

// Expose manifest as a public HTTP route when WORKFLOW_PUBLIC_MANIFEST=1
if (process.env.WORKFLOW_PUBLIC_MANIFEST === '1') {
// Write a placeholder handler file so rollup can resolve the path
// during prod compilation. It will be overwritten with the real
// manifest content by writeManifestHandler() in build:before.
if (!nitro.options.dev) {
const dir = join(nitro.options.buildDir, 'workflow');
mkdirSync(dir, { recursive: true });
const handlerPath = join(dir, 'manifest-handler.mjs');
writeFileSync(
handlerPath,
'export default async () => new Response("Manifest not found", { status: 404 });\n'
);
}
addManifestHandler(nitro);
}
},
} satisfies NitroModule;
Expand Down
6 changes: 3 additions & 3 deletions packages/nitro/src/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import type { Nitro } from 'nitro/types';
import type {} from 'nitro/vite';
import { join } from 'pathe';
import type { Plugin } from 'vite';
import { LocalBuilder } from './builders.js';
import { WorkflowBuilder } from './builders.js';
import type { ModuleOptions } from './index.js';
import nitroModule from './index.js';

export function workflow(options?: ModuleOptions): Plugin[] {
let builder: LocalBuilder;
let builder: WorkflowBuilder;
let workflowBuildDir: string;
const enqueue = createBuildQueue();

Expand Down Expand Up @@ -42,7 +42,7 @@ export function workflow(options?: ModuleOptions): Plugin[] {
_vite: true,
};
if (nitro.options.dev) {
builder = new LocalBuilder(nitro);
builder = new WorkflowBuilder(nitro);
}
return nitroModule.setup(nitro);
},
Expand Down
Loading
Loading