-
Notifications
You must be signed in to change notification settings - Fork 325
Expand file tree
/
Copy pathbuilders.ts
More file actions
127 lines (118 loc) · 4.78 KB
/
Copy pathbuilders.ts
File metadata and controls
127 lines (118 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import {
BaseBuilder,
createBaseBuilderConfig,
VercelBuildOutputAPIBuilder,
} from '@workflow/builders';
import type { Nitro } from 'nitro/types';
import { join } from 'pathe';
/**
* Forward string entries from Nitro's `externals.external` config to the
* workflow builder's esbuild `external` option. RegExp and function entries
* are skipped since esbuild's `external` only supports literal strings.
*
* Note: `externals.external` is on Nitro v2's options shape — v3 dropped it
* in favour of `noExternals`. Reading it through a v2-shaped view lets us
* still pick it up on v2 setups; on v3 the chained optional access just
* returns undefined.
*/
type NitroV2ExternalsOptions = { externals?: { external?: unknown[] } };
function getNitroStringExternals(nitro: Nitro): string[] | undefined {
const external = (nitro.options as NitroV2ExternalsOptions).externals
?.external;
const strings = external?.filter(
(entry): entry is string => typeof entry === 'string'
);
return strings && strings.length > 0 ? strings : undefined;
}
export class VercelBuilder extends VercelBuildOutputAPIBuilder {
constructor(nitro: Nitro) {
super({
...createBaseBuilderConfig({
workingDir: nitro.options.rootDir,
dirs: ['.'], // Different apps that use nitro have different directories
runtime: nitro.options.workflow?.runtime,
sourcemap: nitro.options.workflow?.sourcemap,
externalPackages: getNitroStringExternals(nitro),
}),
buildTarget: 'vercel-build-output-api',
});
}
override async build(): Promise<void> {
const configPath = join(
this.config.workingDir,
'.vercel/output/config.json'
);
const originalConfig = JSON.parse(await readFile(configPath, 'utf-8'));
await super.build();
const newConfig = JSON.parse(await readFile(configPath, 'utf-8'));
originalConfig.routes.unshift(...newConfig.routes);
await writeFile(configPath, JSON.stringify(originalConfig, null, 2));
}
}
export class LocalBuilder extends BaseBuilder {
#outDir: string;
constructor(nitro: Nitro) {
const outDir = join(nitro.options.buildDir, 'workflow');
super({
...createBaseBuilderConfig({
workingDir: nitro.options.rootDir,
watch: nitro.options.dev,
dirs: ['.'], // Different apps that use nitro have different directories
sourcemap: nitro.options.workflow?.sourcemap,
externalPackages: getNitroStringExternals(nitro),
}),
buildTarget: 'next', // Placeholder, not actually used
});
this.#outDir = outDir;
}
// Serialize concurrent build() calls so overlapping dev rebuilds don't
// stomp on each other's temp files or partially overwrite output.
#buildQueue: Promise<void> = Promise.resolve();
override build(): Promise<void> {
const next = this.#buildQueue.then(
() => this.#buildOnce(),
() => this.#buildOnce()
);
// Swallow rejections on the queue itself so a failed build doesn't
// permanently reject all subsequent builds; each caller still sees
// its own rejection via the returned promise.
this.#buildQueue = next.catch(() => {});
return next;
}
async #buildOnce(): Promise<void> {
const inputFiles = await this.getInputFiles();
await mkdir(this.#outDir, { recursive: true });
// V2: The combined bundle's flow route references the steps file by
// name in its import statement, so we build directly to final names.
// (The V1 atomic tmp-file pattern doesn't work here because renaming
// the steps file would leave the flow route's import stale.)
const { manifest } = await this.createCombinedBundle({
inputFiles,
stepsOutfile: join(this.#outDir, 'steps.mjs'),
flowOutfile: join(this.#outDir, 'workflows.mjs'),
format: 'esm',
// bundleFinalOutput: false — Nitro externalizes the workflow build dir
// during dev, and its own rollup pipeline handles bundling for prod.
// Using true causes "Dynamic require of X is not supported" errors
// because esbuild wraps CJS require() calls in ESM output.
bundleFinalOutput: false,
externalizeNonSteps: true,
// In dev, Nitro dynamically imports the generated workflow files from
// disk, so there is no later Rollup pass to resolve externalized local
// TypeScript imports. In prod, Nitro/Rollup handles those imports.
bundleTransitiveLocalStepDependencies: this.config.watch,
});
await this.createWebhookBundle({
outfile: join(this.#outDir, 'webhook.mjs'),
bundle: false,
});
// Generate manifest
const workflowBundlePath = join(this.#outDir, 'workflows.mjs');
await this.createManifest({
workflowBundlePath,
manifestDir: this.#outDir,
manifest,
});
}
}