deepline 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -0
- package/dist/cli/index.js +6750 -503
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6735 -512
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +2349 -32
- package/dist/index.d.ts +2349 -32
- package/dist/index.js +1631 -82
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1617 -83
- package/dist/index.mjs.map +1 -1
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +3256 -0
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +710 -0
- package/dist/repo/apps/play-runner-workers/src/entry.ts +5070 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/README.md +21 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +177 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +52 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +100 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +184 -0
- package/dist/repo/sdk/src/cli/commands/auth.ts +482 -0
- package/dist/repo/sdk/src/cli/commands/billing.ts +188 -0
- package/dist/repo/sdk/src/cli/commands/csv.ts +123 -0
- package/dist/repo/sdk/src/cli/commands/db.ts +119 -0
- package/dist/repo/sdk/src/cli/commands/feedback.ts +40 -0
- package/dist/repo/sdk/src/cli/commands/org.ts +117 -0
- package/dist/repo/sdk/src/cli/commands/play.ts +3200 -0
- package/dist/repo/sdk/src/cli/commands/tools.ts +687 -0
- package/dist/repo/sdk/src/cli/dataset-stats.ts +341 -0
- package/dist/repo/sdk/src/cli/index.ts +138 -0
- package/dist/repo/sdk/src/cli/progress.ts +135 -0
- package/dist/repo/sdk/src/cli/trace.ts +61 -0
- package/dist/repo/sdk/src/cli/utils.ts +145 -0
- package/dist/repo/sdk/src/client.ts +1188 -0
- package/dist/repo/sdk/src/compat.ts +77 -0
- package/dist/repo/sdk/src/config.ts +285 -0
- package/dist/repo/sdk/src/errors.ts +125 -0
- package/dist/repo/sdk/src/http.ts +391 -0
- package/dist/repo/sdk/src/index.ts +139 -0
- package/dist/repo/sdk/src/play.ts +1330 -0
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +133 -0
- package/dist/repo/sdk/src/plays/harness-stub.ts +210 -0
- package/dist/repo/sdk/src/plays/local-file-discovery.ts +326 -0
- package/dist/repo/sdk/src/tool-output.ts +489 -0
- package/dist/repo/sdk/src/types.ts +669 -0
- package/dist/repo/sdk/src/version.ts +2 -0
- package/dist/repo/sdk/src/worker-play-entry.ts +286 -0
- package/dist/repo/shared_libs/observability/node-tracing.ts +129 -0
- package/dist/repo/shared_libs/observability/tracing.ts +98 -0
- package/dist/repo/shared_libs/play-runtime/backend.ts +139 -0
- package/dist/repo/shared_libs/play-runtime/batch-runtime.ts +182 -0
- package/dist/repo/shared_libs/play-runtime/batching-types.ts +91 -0
- package/dist/repo/shared_libs/play-runtime/context.ts +3999 -0
- package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +78 -0
- package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +250 -0
- package/dist/repo/shared_libs/play-runtime/ctx-types.ts +713 -0
- package/dist/repo/shared_libs/play-runtime/dataset-id.ts +10 -0
- package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +304 -0
- package/dist/repo/shared_libs/play-runtime/db-session.ts +462 -0
- package/dist/repo/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/repo/shared_libs/play-runtime/default-batch-strategies.ts +124 -0
- package/dist/repo/shared_libs/play-runtime/execution-plan.ts +262 -0
- package/dist/repo/shared_libs/play-runtime/live-events.ts +214 -0
- package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +50 -0
- package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +114 -0
- package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +158 -0
- package/dist/repo/shared_libs/play-runtime/profiles.ts +90 -0
- package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +172 -0
- package/dist/repo/shared_libs/play-runtime/protocol.ts +121 -0
- package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +42 -0
- package/dist/repo/shared_libs/play-runtime/result-normalization.ts +33 -0
- package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +208 -0
- package/dist/repo/shared_libs/play-runtime/runtime-api.ts +1873 -0
- package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +2 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +201 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +48 -0
- package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +84 -0
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +174 -0
- package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +147 -0
- package/dist/repo/shared_libs/play-runtime/suspension.ts +68 -0
- package/dist/repo/shared_libs/play-runtime/tool-batch-executor.ts +146 -0
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +387 -0
- package/dist/repo/shared_libs/play-runtime/tracing.ts +31 -0
- package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +75 -0
- package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +140 -0
- package/dist/repo/shared_libs/plays/artifact-transport.ts +14 -0
- package/dist/repo/shared_libs/plays/artifact-types.ts +49 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +1346 -0
- package/dist/repo/shared_libs/plays/compiler-manifest.ts +186 -0
- package/dist/repo/shared_libs/plays/contracts.ts +51 -0
- package/dist/repo/shared_libs/plays/dataset.ts +308 -0
- package/dist/repo/shared_libs/plays/definition.ts +264 -0
- package/dist/repo/shared_libs/plays/file-refs.ts +11 -0
- package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +206 -0
- package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +164 -0
- package/dist/repo/shared_libs/plays/row-identity.ts +302 -0
- package/dist/repo/shared_libs/plays/runtime-validation.ts +415 -0
- package/dist/repo/shared_libs/plays/static-pipeline.ts +560 -0
- package/dist/repo/shared_libs/temporal/constants.ts +39 -0
- package/dist/repo/shared_libs/temporal/preview-config.ts +153 -0
- package/package.json +14 -12
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { builtinModules, createRequire } from 'node:module';
|
|
7
|
+
import { build, type Message, type Plugin } from 'esbuild';
|
|
8
|
+
import ts from 'typescript';
|
|
9
|
+
import {
|
|
10
|
+
PLAY_ARTIFACT_KINDS,
|
|
11
|
+
type PlayArtifactKind,
|
|
12
|
+
} from '../../play-runtime/backend.js';
|
|
13
|
+
import type { PlayCompilerManifest } from '../compiler-manifest.js';
|
|
14
|
+
import type {
|
|
15
|
+
PlayArtifactCompatibility,
|
|
16
|
+
PlayBundleArtifact,
|
|
17
|
+
PlayImportPolicy,
|
|
18
|
+
PlayPackageImport,
|
|
19
|
+
PlayRuntimeFeature,
|
|
20
|
+
} from '../artifact-types.js';
|
|
21
|
+
import { buildPlayContractCompatibility } from '../contracts.js';
|
|
22
|
+
|
|
23
|
+
const playArtifactRequire = createRequire(import.meta.url);
|
|
24
|
+
const PLAY_BUNDLE_CACHE_VERSION = 24;
|
|
25
|
+
const MAX_PLAY_BUNDLE_BYTES = 30 * 1024 * 1024;
|
|
26
|
+
// workerd local-mode (`wrangler dev` Worker Loader) silently fails to
|
|
27
|
+
// instantiate per-graphHash play Workers when the bundled code passes a
|
|
28
|
+
// threshold somewhere between 1.04 MiB (44-package-imports — works) and
|
|
29
|
+
// 1.18 MiB (the same play with date-fns added — hangs forever). The
|
|
30
|
+
// workflow body never runs, no error is logged anywhere, and the run
|
|
31
|
+
// hangs indefinitely. We surface this as a hard bundle failure so the
|
|
32
|
+
// user gets an actionable message at submit time instead of a 5-minute
|
|
33
|
+
// silent timeout. Real CF (workers.dev) accepts much larger bundles, but
|
|
34
|
+
// `dev:v2 cloudflare` is the regression entrypoint so the local limit is
|
|
35
|
+
// the binding one.
|
|
36
|
+
const MAX_ESM_WORKERS_BUNDLE_BYTES = 1_150_000;
|
|
37
|
+
const PLAY_ARTIFACT_CACHE_DIR = join(
|
|
38
|
+
tmpdir(),
|
|
39
|
+
`deepline-play-artifacts-v${PLAY_BUNDLE_CACHE_VERSION}`,
|
|
40
|
+
);
|
|
41
|
+
const PLAY_PROXY_NAMESPACE = 'deepline-play-runtime-ref';
|
|
42
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json'];
|
|
43
|
+
const WORKERS_PLAY_ENTRY_VIRTUAL = 'deepline-play-entry';
|
|
44
|
+
const PLAY_SOURCE_FILE_PATTERN = /\.play\.(?:[cm]?[jt]sx?)$/i;
|
|
45
|
+
const NODE_BUILTIN_SET = new Set(
|
|
46
|
+
builtinModules.flatMap((name) => (name.startsWith('node:') ? [name, name.slice(5)] : [name, `node:${name}`])),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export type {
|
|
50
|
+
PlayArtifactCompatibility,
|
|
51
|
+
PlayBundleArtifact,
|
|
52
|
+
PlayImportPolicy,
|
|
53
|
+
PlayPackageImport,
|
|
54
|
+
PlayRuntimeFeature,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ImportedPlayDependency = {
|
|
58
|
+
filePath: string;
|
|
59
|
+
playName: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type PlayLocalFileReference = {
|
|
63
|
+
sourceFragment: string;
|
|
64
|
+
logicalPath: string;
|
|
65
|
+
absolutePath: string;
|
|
66
|
+
bytes: number;
|
|
67
|
+
contentHash: string;
|
|
68
|
+
contentType: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type PlayLocalFileDiscoveryError = {
|
|
72
|
+
sourceFragment: string;
|
|
73
|
+
message: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type PlayLocalFileDiscoveryResult = {
|
|
77
|
+
files: PlayLocalFileReference[];
|
|
78
|
+
unresolved: PlayLocalFileDiscoveryError[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type PlayBundlingAdapter = {
|
|
82
|
+
projectRoot: string;
|
|
83
|
+
nodeModulesDir: string;
|
|
84
|
+
cacheDir?: string;
|
|
85
|
+
sdkSourceRoot: string;
|
|
86
|
+
sdkPackageJson: string;
|
|
87
|
+
sdkEntryFile: string;
|
|
88
|
+
sdkTypesEntryFile?: string;
|
|
89
|
+
sdkWorkersEntryFile: string;
|
|
90
|
+
workersHarnessEntryFile: string;
|
|
91
|
+
workersHarnessFilesDir: string;
|
|
92
|
+
discoverPackagedLocalFiles(filePath: string): Promise<PlayLocalFileDiscoveryResult>;
|
|
93
|
+
typecheckSdkTypes?: boolean;
|
|
94
|
+
typecheckPlaySource?(input: {
|
|
95
|
+
sourceCode: string;
|
|
96
|
+
sourcePath: string;
|
|
97
|
+
importedFilePaths: string[];
|
|
98
|
+
}): Promise<string[]> | string[];
|
|
99
|
+
warnAboutNonDevelopmentBundling?(filePath: string): void;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type BundledPlayFileSuccess = {
|
|
103
|
+
success: true;
|
|
104
|
+
artifact: PlayBundleArtifact;
|
|
105
|
+
sourceCode: string;
|
|
106
|
+
filePath: string;
|
|
107
|
+
playName: string | null;
|
|
108
|
+
compilerManifest?: PlayCompilerManifest;
|
|
109
|
+
packagedFiles: PlayLocalFileReference[];
|
|
110
|
+
unresolvedFileReferences: PlayLocalFileDiscoveryError[];
|
|
111
|
+
importedPlayDependencies: ImportedPlayDependency[];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type BundledPlayFileFailure = {
|
|
115
|
+
success: false;
|
|
116
|
+
errors: string[];
|
|
117
|
+
filePath: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type BundledPlayFileResult =
|
|
121
|
+
| BundledPlayFileSuccess
|
|
122
|
+
| BundledPlayFileFailure;
|
|
123
|
+
|
|
124
|
+
type PackageResolution = {
|
|
125
|
+
name: string;
|
|
126
|
+
version: string | null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
type SourceGraphAnalysis = {
|
|
130
|
+
sourceCode: string;
|
|
131
|
+
sourceHash: string;
|
|
132
|
+
graphHash: string;
|
|
133
|
+
importPolicy: PlayImportPolicy;
|
|
134
|
+
playName: string | null;
|
|
135
|
+
importedPlayDependencies: ImportedPlayDependency[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type PlayWorkspace = {
|
|
139
|
+
entryFile: string;
|
|
140
|
+
rootDir: string;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function sha256(value: string): string {
|
|
144
|
+
return createHash('sha256').update(value).digest('hex');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatEsbuildMessage(message: Message): string {
|
|
148
|
+
const location = message.location
|
|
149
|
+
? `${message.location.file}:${message.location.line}:${message.location.column}`
|
|
150
|
+
: null;
|
|
151
|
+
return location ? `${location} ${message.text}` : message.text;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatTypeScriptDiagnostic(diagnostic: ts.Diagnostic): string | null {
|
|
155
|
+
if (!diagnostic.file) {
|
|
156
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n').trim();
|
|
157
|
+
return message || null;
|
|
158
|
+
}
|
|
159
|
+
const start = diagnostic.start ?? 0;
|
|
160
|
+
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(start);
|
|
161
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n').trim();
|
|
162
|
+
if (!message) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return `${diagnostic.file.fileName}:${line + 1}:${character + 1} ${message}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function typecheckPlaySource(
|
|
169
|
+
input: SourceGraphAnalysis,
|
|
170
|
+
adapter: PlayBundlingAdapter,
|
|
171
|
+
): string[] {
|
|
172
|
+
const rootNames = Array.from(
|
|
173
|
+
new Set([
|
|
174
|
+
...input.importPolicy.localFiles,
|
|
175
|
+
...input.importedPlayDependencies.map((dependency) => dependency.filePath),
|
|
176
|
+
]),
|
|
177
|
+
);
|
|
178
|
+
// Resolve `deepline` to SDK source, not ignored dist artifacts. The source
|
|
179
|
+
// package surface is the canonical local play authoring contract.
|
|
180
|
+
const sdkTypesPath = adapter.sdkTypesEntryFile ?? adapter.sdkEntryFile;
|
|
181
|
+
const program = ts.createProgram(rootNames, {
|
|
182
|
+
target: ts.ScriptTarget.ES2023,
|
|
183
|
+
// SDK source uses fetch/RequestInit/URL and node-aware config helpers.
|
|
184
|
+
// The play runtime import policy below still bans Node modules from play
|
|
185
|
+
// source for workers_edge bundles.
|
|
186
|
+
lib: ['lib.es2023.d.ts', 'lib.dom.d.ts'],
|
|
187
|
+
module: ts.ModuleKind.ESNext,
|
|
188
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
189
|
+
paths: { deepline: [sdkTypesPath] },
|
|
190
|
+
strict: true,
|
|
191
|
+
skipLibCheck: true,
|
|
192
|
+
noEmit: true,
|
|
193
|
+
esModuleInterop: true,
|
|
194
|
+
allowSyntheticDefaultImports: true,
|
|
195
|
+
allowImportingTsExtensions: true,
|
|
196
|
+
allowJs: true,
|
|
197
|
+
resolveJsonModule: true,
|
|
198
|
+
types: ['node'],
|
|
199
|
+
});
|
|
200
|
+
return ts
|
|
201
|
+
.getPreEmitDiagnostics(program)
|
|
202
|
+
.map(formatTypeScriptDiagnostic)
|
|
203
|
+
.filter((message): message is string => Boolean(message));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isLocalSpecifier(specifier: string): boolean {
|
|
207
|
+
return (
|
|
208
|
+
specifier.startsWith('./') ||
|
|
209
|
+
specifier.startsWith('../') ||
|
|
210
|
+
specifier.startsWith('/') ||
|
|
211
|
+
specifier.startsWith('file:')
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function normalizeLocalPath(filePath: string): Promise<string> {
|
|
216
|
+
try {
|
|
217
|
+
return await realpath(filePath);
|
|
218
|
+
} catch {
|
|
219
|
+
return resolve(filePath);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function createPlayWorkspace(entryFile: string): PlayWorkspace {
|
|
224
|
+
return {
|
|
225
|
+
entryFile,
|
|
226
|
+
rootDir: dirname(entryFile),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isPathInsideDirectory(filePath: string, directory: string): boolean {
|
|
231
|
+
return filePath === directory || filePath.startsWith(`${directory}/`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function assertWithinPlayWorkspace(input: {
|
|
235
|
+
importer: string;
|
|
236
|
+
specifier: string;
|
|
237
|
+
resolvedPath: string;
|
|
238
|
+
workspace: PlayWorkspace;
|
|
239
|
+
sourceFile: ts.SourceFile;
|
|
240
|
+
node: ts.Node;
|
|
241
|
+
}): void {
|
|
242
|
+
if (isPathInsideDirectory(input.resolvedPath, input.workspace.rootDir)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const position = input.sourceFile.getLineAndCharacterOfPosition(
|
|
247
|
+
input.node.getStart(input.sourceFile),
|
|
248
|
+
);
|
|
249
|
+
throw new Error(
|
|
250
|
+
`${input.importer}:${position.line + 1}:${position.character + 1} ` +
|
|
251
|
+
`Local play imports must stay inside the play workspace (${input.workspace.rootDir}). ` +
|
|
252
|
+
`Import "${input.specifier}" resolved to ${input.resolvedPath}, which crosses into app/backend code. ` +
|
|
253
|
+
'Use the public SDK/API surface or move shared helpers into the play workspace.',
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getPackageName(specifier: string): string {
|
|
258
|
+
if (specifier.startsWith('@')) {
|
|
259
|
+
const [scope, name] = specifier.split('/');
|
|
260
|
+
return scope && name ? `${scope}/${name}` : specifier;
|
|
261
|
+
}
|
|
262
|
+
return specifier.split('/')[0] ?? specifier;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function scriptKindForFile(filePath: string): ts.ScriptKind {
|
|
266
|
+
const extension = extname(filePath).toLowerCase();
|
|
267
|
+
switch (extension) {
|
|
268
|
+
case '.tsx':
|
|
269
|
+
return ts.ScriptKind.TSX;
|
|
270
|
+
case '.jsx':
|
|
271
|
+
return ts.ScriptKind.JSX;
|
|
272
|
+
case '.js':
|
|
273
|
+
case '.mjs':
|
|
274
|
+
case '.cjs':
|
|
275
|
+
return ts.ScriptKind.JS;
|
|
276
|
+
case '.json':
|
|
277
|
+
return ts.ScriptKind.JSON;
|
|
278
|
+
default:
|
|
279
|
+
return ts.ScriptKind.TS;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isPlaySourceFile(filePath: string): boolean {
|
|
284
|
+
return PLAY_SOURCE_FILE_PATTERN.test(filePath);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function extractStringLiteralProperty(
|
|
288
|
+
objectLiteral: ts.ObjectLiteralExpression,
|
|
289
|
+
propertyName: string,
|
|
290
|
+
): string | null {
|
|
291
|
+
for (const property of objectLiteral.properties) {
|
|
292
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const name = property.name;
|
|
296
|
+
const matches =
|
|
297
|
+
(ts.isIdentifier(name) && name.text === propertyName) ||
|
|
298
|
+
(ts.isStringLiteralLike(name) && name.text === propertyName);
|
|
299
|
+
if (!matches) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
return ts.isStringLiteralLike(property.initializer)
|
|
303
|
+
? property.initializer.text.trim()
|
|
304
|
+
: null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function extractDefinedPlayName(
|
|
311
|
+
sourceCode: string,
|
|
312
|
+
filePath: string,
|
|
313
|
+
): string | null {
|
|
314
|
+
const sourceFile = ts.createSourceFile(
|
|
315
|
+
filePath,
|
|
316
|
+
sourceCode,
|
|
317
|
+
ts.ScriptTarget.Latest,
|
|
318
|
+
true,
|
|
319
|
+
scriptKindForFile(filePath),
|
|
320
|
+
);
|
|
321
|
+
let detectedPlayName: string | null = null;
|
|
322
|
+
|
|
323
|
+
const visit = (node: ts.Node) => {
|
|
324
|
+
if (detectedPlayName) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (ts.isCallExpression(node)) {
|
|
329
|
+
const expression = node.expression;
|
|
330
|
+
const isDefinePlayCall =
|
|
331
|
+
(ts.isIdentifier(expression) &&
|
|
332
|
+
(expression.text === 'definePlay' || expression.text === 'defineWorkflow')) ||
|
|
333
|
+
(ts.isPropertyAccessExpression(expression) &&
|
|
334
|
+
(expression.name.text === 'definePlay' ||
|
|
335
|
+
expression.name.text === 'defineWorkflow'));
|
|
336
|
+
if (isDefinePlayCall) {
|
|
337
|
+
const firstArgument = node.arguments[0];
|
|
338
|
+
if (firstArgument && ts.isStringLiteralLike(firstArgument)) {
|
|
339
|
+
detectedPlayName = firstArgument.text.trim() || null;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (firstArgument && ts.isObjectLiteralExpression(firstArgument)) {
|
|
343
|
+
detectedPlayName = extractStringLiteralProperty(firstArgument, 'id');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
ts.forEachChild(node, visit);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
visit(sourceFile);
|
|
353
|
+
return detectedPlayName;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getPackageRequireCandidates(fromFile: string) {
|
|
357
|
+
const candidates = [
|
|
358
|
+
createRequire(fromFile),
|
|
359
|
+
createRequire(join(process.cwd(), 'package.json')),
|
|
360
|
+
playArtifactRequire,
|
|
361
|
+
];
|
|
362
|
+
return candidates;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function localSdkAliasPlugin(
|
|
366
|
+
adapter: PlayBundlingAdapter,
|
|
367
|
+
options?: { workersRuntime?: boolean },
|
|
368
|
+
): Plugin | null {
|
|
369
|
+
const entryFile = options?.workersRuntime
|
|
370
|
+
? adapter.sdkWorkersEntryFile
|
|
371
|
+
: adapter.sdkEntryFile;
|
|
372
|
+
if (!playArtifactRequire('node:fs').existsSync(entryFile)) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
name: 'deepline-sdk-local-alias',
|
|
378
|
+
setup(buildContext) {
|
|
379
|
+
buildContext.onResolve({ filter: /^deepline$/ }, () => ({
|
|
380
|
+
path: entryFile,
|
|
381
|
+
}));
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Resolves the virtual `deepline-play-entry` import (used by the Workers
|
|
388
|
+
* harness) to the user's play source file. Lets the harness statically import
|
|
389
|
+
* the play without the bundler needing to know its path ahead of time.
|
|
390
|
+
*/
|
|
391
|
+
function workersPlayEntryAliasPlugin(playFilePath: string): Plugin {
|
|
392
|
+
return {
|
|
393
|
+
name: 'deepline-workers-play-entry-alias',
|
|
394
|
+
setup(buildContext) {
|
|
395
|
+
buildContext.onResolve(
|
|
396
|
+
{ filter: new RegExp(`^${WORKERS_PLAY_ENTRY_VIRTUAL}$`) },
|
|
397
|
+
() => ({ path: playFilePath }),
|
|
398
|
+
);
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Cloudflare Workers' `nodejs_compat` flag covers most node builtins (path,
|
|
405
|
+
* crypto, buffer, async_hooks, ...) but NOT `node:fs` / `node:fs/promises` /
|
|
406
|
+
* `node:os` (no filesystem, no OS in a V8 isolate). The SDK's config-loading
|
|
407
|
+
* and tool-output utilities reference these even when the play never invokes
|
|
408
|
+
* them — which is the case for plays that don't read CLI config at runtime.
|
|
409
|
+
*
|
|
410
|
+
* This plugin substitutes those specific imports with a tiny stub module that
|
|
411
|
+
* has no top-level side effects and throws clearly if anything actually calls
|
|
412
|
+
* into them. That lets the deploy validation pass while still surfacing a
|
|
413
|
+
* loud error if a play unexpectedly depends on a node-only API.
|
|
414
|
+
*/
|
|
415
|
+
function workersNodeBuiltinStubPlugin(): Plugin {
|
|
416
|
+
// These are the node builtins NOT exposed by Cloudflare's nodejs_compat.
|
|
417
|
+
// Keep this list narrow — anything supported (path, crypto, etc.) should
|
|
418
|
+
// pass through as `external: ['node:*']` in the build config.
|
|
419
|
+
const UNSUPPORTED = new Set(['node:fs', 'node:fs/promises', 'node:os', 'node:child_process']);
|
|
420
|
+
return {
|
|
421
|
+
name: 'deepline-workers-node-builtin-stub',
|
|
422
|
+
setup(buildContext) {
|
|
423
|
+
buildContext.onResolve({ filter: /^node:/ }, (args) => {
|
|
424
|
+
if (!UNSUPPORTED.has(args.path)) return null;
|
|
425
|
+
return { path: args.path, namespace: 'deepline-workers-node-stub' };
|
|
426
|
+
});
|
|
427
|
+
buildContext.onLoad(
|
|
428
|
+
{ filter: /.*/, namespace: 'deepline-workers-node-stub' },
|
|
429
|
+
(args) => {
|
|
430
|
+
const builtinName = args.path;
|
|
431
|
+
// CommonJS module.exports = Proxy. esbuild's CJS↔ESM interop maps
|
|
432
|
+
// any named import (e.g. `import { readFileSync } from 'node:fs'`)
|
|
433
|
+
// to a property access on this Proxy. Property access returns a
|
|
434
|
+
// function-like Proxy that throws on call. So:
|
|
435
|
+
// - import resolves at bundle time (deploy validation passes)
|
|
436
|
+
// - import binding evaluates to a no-arg-throw function
|
|
437
|
+
// - calling it gives a clear error
|
|
438
|
+
const stubSource = `
|
|
439
|
+
const message = ${JSON.stringify(
|
|
440
|
+
`Workers backend: ${builtinName} is not available in Cloudflare Workers ` +
|
|
441
|
+
'(no filesystem / no OS). Run this play on Daytona or local-process backend ' +
|
|
442
|
+
'if it needs node builtins.',
|
|
443
|
+
)};
|
|
444
|
+
function makeThrower(name) {
|
|
445
|
+
return new Proxy(function() { throw new Error(message + ' (called: ' + name + ')'); }, {
|
|
446
|
+
get(_t, prop) {
|
|
447
|
+
if (prop === '__esModule') return true;
|
|
448
|
+
if (typeof prop === 'symbol') return undefined;
|
|
449
|
+
return makeThrower(name + '.' + String(prop));
|
|
450
|
+
},
|
|
451
|
+
apply() { throw new Error(message + ' (called: ' + name + ')'); },
|
|
452
|
+
construct() { throw new Error(message + ' (constructed: ' + name + ')'); },
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
module.exports = makeThrower(${JSON.stringify(builtinName)});
|
|
456
|
+
`;
|
|
457
|
+
return { contents: stubSource, loader: 'js' };
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Stub every `zod/v4/locales/<lang>.js` module EXCEPT English with an
|
|
466
|
+
* empty default export so non-English locale strings don't get bundled
|
|
467
|
+
* into per-play Workers.
|
|
468
|
+
*
|
|
469
|
+
* Why this exists:
|
|
470
|
+
* `zod/v4/core/index.js` does `export * as locales from "../locales/index.js"`
|
|
471
|
+
* and `zod/v4/locales/index.js` re-exports every individual locale
|
|
472
|
+
* module (he, ru, ta, th, be, km, ka, uk, hy, bg, ur, ar, mk, fa, ps,
|
|
473
|
+
* lt, …). esbuild follows the static re-exports and bundles every
|
|
474
|
+
* locale, ~5–9 KB each. A bundle audit (`scripts/audit-play-bundle.ts`)
|
|
475
|
+
* on `03-tool-basic.play.ts` showed ~22 of these locale modules
|
|
476
|
+
* present, totaling ~150–200 KB of dead weight.
|
|
477
|
+
*
|
|
478
|
+
* Plays only ever surface English error messages to user/CLI. The
|
|
479
|
+
* non-English locale data is unreachable in our codepath, but
|
|
480
|
+
* esbuild's tree-shaking can't prove that across `export * as`
|
|
481
|
+
* re-exports. So we stub each non-English locale at load time with
|
|
482
|
+
* an empty default export. zod's runtime falls back to its built-in
|
|
483
|
+
* English messages when a requested locale's data is missing or
|
|
484
|
+
* empty.
|
|
485
|
+
*
|
|
486
|
+
* What this saves (measured, fresh per-play bundle):
|
|
487
|
+
* ~150–200 KB → cold V8 compile time drops proportionally
|
|
488
|
+
* (~50–70 ms on CF edge per cold play).
|
|
489
|
+
*
|
|
490
|
+
* Safety:
|
|
491
|
+
* - English (`en.js`) is NEVER stubbed — that's the language zod
|
|
492
|
+
* and our error surfaces actually use.
|
|
493
|
+
* - The locales/index.js wrapper is left intact; its re-exports
|
|
494
|
+
* resolve to empty `{}` modules but the import shape stays
|
|
495
|
+
* stable for any code that does `import { locales } from "zod"`.
|
|
496
|
+
* - If a future codepath needs a non-English locale, this plugin
|
|
497
|
+
* would silently swallow the data; that would be a real bug, so
|
|
498
|
+
* we log a one-time warning at bundle time when a non-English
|
|
499
|
+
* locale is stubbed (only emitted to stderr in dev to keep CI
|
|
500
|
+
* output clean).
|
|
501
|
+
*/
|
|
502
|
+
function zodNonEnglishLocaleStubPlugin(): Plugin {
|
|
503
|
+
// Match any zod v4 locale module path EXCEPT en + en variants.
|
|
504
|
+
// The filter uses a coarse regex; the namespace handler then
|
|
505
|
+
// double-checks with a precise basename test before stubbing.
|
|
506
|
+
const LOCALE_PATH_FILTER = /[\\/]zod[\\/]v4[\\/]locales[\\/][^\\/]+\.(?:c?js|mjs)$/;
|
|
507
|
+
const NAMESPACE = 'deepline-zod-locale-stub';
|
|
508
|
+
return {
|
|
509
|
+
name: 'deepline-zod-non-english-locale-stub',
|
|
510
|
+
setup(buildContext) {
|
|
511
|
+
buildContext.onResolve({ filter: LOCALE_PATH_FILTER }, (args) => {
|
|
512
|
+
// Resolve relatively first (esbuild needs an absolute path), then
|
|
513
|
+
// decide whether to stub. We do this in onResolve so we can check
|
|
514
|
+
// the filename and let `en` flow through unmodified.
|
|
515
|
+
const norm = args.path.replace(/\\/g, '/');
|
|
516
|
+
const file = norm.slice(norm.lastIndexOf('/') + 1);
|
|
517
|
+
// Allow English variants — `en.js`, `en.cjs`, future `en-US.js`, etc.
|
|
518
|
+
if (/^en(?:[-_][A-Za-z]+)?\.(?:c?js|mjs)$/.test(file)) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
// Anything else: stub with an empty default export.
|
|
522
|
+
return { path: args.path, namespace: NAMESPACE };
|
|
523
|
+
});
|
|
524
|
+
buildContext.onLoad(
|
|
525
|
+
{ filter: /.*/, namespace: NAMESPACE },
|
|
526
|
+
() => ({
|
|
527
|
+
// zod locales export a default object literal. Empty object is
|
|
528
|
+
// structurally compatible — accessing any locale key returns
|
|
529
|
+
// undefined and zod falls back to its built-in English.
|
|
530
|
+
contents: 'export default {};',
|
|
531
|
+
loader: 'js',
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Hard banlist of `node:*` modules whose presence in a workers_edge bundle
|
|
540
|
+
* would either escape the V8 isolate boundary (filesystem, network sockets,
|
|
541
|
+
* subprocess control) or open up confused-deputy paths (vm/repl/worker
|
|
542
|
+
* threads). These are blocked at bundle time so the per-graphHash CF Worker
|
|
543
|
+
* never sees code that could attempt to call them.
|
|
544
|
+
*
|
|
545
|
+
* `nodejs_compat` covers the supported subset (crypto, stream, buffer, util,
|
|
546
|
+
* path, url, http, https) — those imports continue to pass through and
|
|
547
|
+
* resolve to workerd-shimmed implementations.
|
|
548
|
+
*/
|
|
549
|
+
const WORKERS_BANNED_NODE_MODULES = new Set([
|
|
550
|
+
'node:fs',
|
|
551
|
+
'node:fs/promises',
|
|
552
|
+
'node:net',
|
|
553
|
+
'node:child_process',
|
|
554
|
+
'node:cluster',
|
|
555
|
+
'node:tls',
|
|
556
|
+
'node:dgram',
|
|
557
|
+
'node:dns',
|
|
558
|
+
'node:dns/promises',
|
|
559
|
+
'node:repl',
|
|
560
|
+
'node:vm',
|
|
561
|
+
'node:worker_threads',
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
function workersNodeImportBanlistPlugin(adapter: PlayBundlingAdapter): Plugin {
|
|
565
|
+
return {
|
|
566
|
+
name: 'deepline-workers-node-import-banlist',
|
|
567
|
+
setup(buildContext) {
|
|
568
|
+
buildContext.onResolve({ filter: /^node:/ }, (args) => {
|
|
569
|
+
if (!WORKERS_BANNED_NODE_MODULES.has(args.path)) return null;
|
|
570
|
+
// SDK internals (sdk/src/*.ts) intentionally reference a small set
|
|
571
|
+
// of node builtins (e.g. config loaders, tool-output writers) that
|
|
572
|
+
// never run in the V8 isolate path; they're substituted by the
|
|
573
|
+
// workersNodeBuiltinStubPlugin's throwing stub. Only reject when
|
|
574
|
+
// the import comes from outside the SDK source tree -- i.e. user
|
|
575
|
+
// play code or third-party deps that the play pulls in.
|
|
576
|
+
const importer = args.importer ?? '';
|
|
577
|
+
if (importer.startsWith(adapter.sdkSourceRoot)) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
errors: [
|
|
582
|
+
{
|
|
583
|
+
text: `Module "${args.path}" is not allowed in workers_edge plays. See AGENTS.md for the V8 isolate boundary.`,
|
|
584
|
+
detail: { importer: args.importer, kind: args.kind },
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function buildImportedPlayProxyModule(playName: string): string {
|
|
594
|
+
const serializedName = JSON.stringify(playName);
|
|
595
|
+
return `
|
|
596
|
+
const PLAY_METADATA_SYMBOL = Symbol.for('deepline.play.metadata');
|
|
597
|
+
const importedPlayRef = async function importedPlayRef(ctx, input) {
|
|
598
|
+
return ctx.runPlay(${JSON.stringify(`imported_${playName.replace(/[^A-Za-z0-9_]+/g, '_')}`)}, importedPlayRef, input, {
|
|
599
|
+
description: 'Run the imported Deepline play dependency.',
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
Object.defineProperty(importedPlayRef, 'playName', {
|
|
603
|
+
value: ${serializedName},
|
|
604
|
+
enumerable: true,
|
|
605
|
+
configurable: false,
|
|
606
|
+
writable: false,
|
|
607
|
+
});
|
|
608
|
+
Object.defineProperty(importedPlayRef, PLAY_METADATA_SYMBOL, {
|
|
609
|
+
value: { name: ${serializedName} },
|
|
610
|
+
enumerable: false,
|
|
611
|
+
configurable: false,
|
|
612
|
+
writable: false,
|
|
613
|
+
});
|
|
614
|
+
export const playName = ${serializedName};
|
|
615
|
+
export const name = ${serializedName};
|
|
616
|
+
export default importedPlayRef;
|
|
617
|
+
`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function importedPlayProxyPlugin(
|
|
621
|
+
importedPlayDependencies: ImportedPlayDependency[],
|
|
622
|
+
): Plugin | null {
|
|
623
|
+
if (importedPlayDependencies.length === 0) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const dependenciesByPath = new Map(
|
|
628
|
+
importedPlayDependencies.map((dependency) => [dependency.filePath, dependency]),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
name: 'deepline-imported-play-proxy',
|
|
633
|
+
setup(buildContext) {
|
|
634
|
+
buildContext.onResolve({ filter: /.*/ }, async (args) => {
|
|
635
|
+
if (!args.importer || !isLocalSpecifier(args.path)) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const resolvedPath = await resolveLocalImport(args.importer, args.path);
|
|
640
|
+
const dependency = dependenciesByPath.get(resolvedPath);
|
|
641
|
+
if (!dependency) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
path: dependency.filePath,
|
|
647
|
+
namespace: PLAY_PROXY_NAMESPACE,
|
|
648
|
+
pluginData: dependency,
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
buildContext.onLoad({ filter: /.*/, namespace: PLAY_PROXY_NAMESPACE }, async (args) => {
|
|
653
|
+
const dependency = (args.pluginData as ImportedPlayDependency | undefined)
|
|
654
|
+
?? dependenciesByPath.get(args.path);
|
|
655
|
+
if (!dependency) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
contents: buildImportedPlayProxyModule(dependency.playName),
|
|
661
|
+
loader: 'ts',
|
|
662
|
+
resolveDir: dirname(args.path),
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
670
|
+
try {
|
|
671
|
+
await stat(filePath);
|
|
672
|
+
return true;
|
|
673
|
+
} catch {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function resolveLocalImport(fromFile: string, specifier: string): Promise<string> {
|
|
679
|
+
if (specifier.startsWith('file:')) {
|
|
680
|
+
return normalizeLocalPath(new URL(specifier).pathname);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const base = isAbsolute(specifier) ? resolve(specifier) : resolve(dirname(fromFile), specifier);
|
|
684
|
+
const candidates: string[] = [base];
|
|
685
|
+
const explicitExtension = extname(base).toLowerCase();
|
|
686
|
+
|
|
687
|
+
if (!explicitExtension) {
|
|
688
|
+
candidates.push(...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`));
|
|
689
|
+
candidates.push(...SOURCE_EXTENSIONS.map((extension) => join(base, `index${extension}`)));
|
|
690
|
+
} else if (['.js', '.jsx', '.mjs', '.cjs'].includes(explicitExtension)) {
|
|
691
|
+
const stem = base.slice(0, -explicitExtension.length);
|
|
692
|
+
candidates.push(...SOURCE_EXTENSIONS.map((extension) => `${stem}${extension}`));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
for (const candidate of candidates) {
|
|
696
|
+
if (await fileExists(candidate)) {
|
|
697
|
+
return normalizeLocalPath(candidate);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
throw new Error(`Could not resolve local import "${specifier}" from ${fromFile}`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function resolvePackageImport(
|
|
705
|
+
specifier: string,
|
|
706
|
+
fromFile: string,
|
|
707
|
+
adapter: PlayBundlingAdapter,
|
|
708
|
+
): PackageResolution {
|
|
709
|
+
const packageName = getPackageName(specifier);
|
|
710
|
+
if (packageName === 'deepline' && playArtifactRequire('node:fs').existsSync(adapter.sdkPackageJson)) {
|
|
711
|
+
const packageJson = JSON.parse(
|
|
712
|
+
playArtifactRequire('node:fs').readFileSync(adapter.sdkPackageJson, 'utf-8'),
|
|
713
|
+
) as { version?: string };
|
|
714
|
+
return {
|
|
715
|
+
name: 'deepline',
|
|
716
|
+
version: packageJson.version ?? null,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const candidateRequires = getPackageRequireCandidates(fromFile);
|
|
720
|
+
let resolved = false;
|
|
721
|
+
|
|
722
|
+
for (const candidateRequire of candidateRequires) {
|
|
723
|
+
try {
|
|
724
|
+
candidateRequire.resolve(specifier);
|
|
725
|
+
resolved = true;
|
|
726
|
+
break;
|
|
727
|
+
} catch {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!resolved) {
|
|
733
|
+
throw new Error(`Could not resolve "${specifier}" from ${fromFile}`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
let version: string | null = null;
|
|
737
|
+
|
|
738
|
+
for (const candidateRequire of candidateRequires) {
|
|
739
|
+
try {
|
|
740
|
+
const packageJsonPath = candidateRequire.resolve(`${packageName}/package.json`);
|
|
741
|
+
const packageJson = JSON.parse(
|
|
742
|
+
playArtifactRequire('node:fs').readFileSync(packageJsonPath, 'utf-8'),
|
|
743
|
+
) as { version?: string };
|
|
744
|
+
version = packageJson.version ?? null;
|
|
745
|
+
break;
|
|
746
|
+
} catch {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return { name: packageName, version };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function analyzeSourceGraph(
|
|
755
|
+
entryFile: string,
|
|
756
|
+
adapter: PlayBundlingAdapter,
|
|
757
|
+
): Promise<SourceGraphAnalysis> {
|
|
758
|
+
const absoluteEntryFile = await normalizeLocalPath(entryFile);
|
|
759
|
+
const workspace = createPlayWorkspace(absoluteEntryFile);
|
|
760
|
+
const localFiles = new Map<string, string>();
|
|
761
|
+
const nodeBuiltins = new Set<string>();
|
|
762
|
+
const packages = new Map<string, string | null>();
|
|
763
|
+
const importedPlayDependencies = new Map<string, ImportedPlayDependency>();
|
|
764
|
+
const visited = new Set<string>();
|
|
765
|
+
|
|
766
|
+
const visitFile = async (filePath: string) => {
|
|
767
|
+
const absolutePath = await normalizeLocalPath(filePath);
|
|
768
|
+
if (visited.has(absolutePath)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
visited.add(absolutePath);
|
|
772
|
+
|
|
773
|
+
const sourceCode = await readFile(absolutePath, 'utf-8');
|
|
774
|
+
localFiles.set(absolutePath, sourceCode);
|
|
775
|
+
|
|
776
|
+
if (extname(absolutePath).toLowerCase() === '.json') {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const sourceFile = ts.createSourceFile(
|
|
781
|
+
absolutePath,
|
|
782
|
+
sourceCode,
|
|
783
|
+
ts.ScriptTarget.Latest,
|
|
784
|
+
true,
|
|
785
|
+
scriptKindForFile(absolutePath),
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
const handleSpecifier = async (
|
|
789
|
+
specifier: string,
|
|
790
|
+
node: ts.Node,
|
|
791
|
+
kind: 'static' | 'require' | 'dynamic-import',
|
|
792
|
+
) => {
|
|
793
|
+
if (kind === 'dynamic-import') {
|
|
794
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
795
|
+
throw new Error(
|
|
796
|
+
`${absolutePath}:${position.line + 1}:${position.character + 1} Dynamic import() is not allowed in plays. Use static imports instead.`,
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (NODE_BUILTIN_SET.has(specifier)) {
|
|
801
|
+
nodeBuiltins.add(specifier.startsWith('node:') ? specifier : `node:${specifier}`);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (isLocalSpecifier(specifier)) {
|
|
806
|
+
const resolved = await resolveLocalImport(absolutePath, specifier);
|
|
807
|
+
assertWithinPlayWorkspace({
|
|
808
|
+
importer: absolutePath,
|
|
809
|
+
specifier,
|
|
810
|
+
resolvedPath: resolved,
|
|
811
|
+
workspace,
|
|
812
|
+
sourceFile,
|
|
813
|
+
node,
|
|
814
|
+
});
|
|
815
|
+
if (resolved !== absoluteEntryFile && isPlaySourceFile(resolved)) {
|
|
816
|
+
const importedSource = await readFile(resolved, 'utf-8');
|
|
817
|
+
const importedPlayName = extractDefinedPlayName(importedSource, resolved);
|
|
818
|
+
if (!importedPlayName) {
|
|
819
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
820
|
+
throw new Error(
|
|
821
|
+
`${absolutePath}:${position.line + 1}:${position.character + 1} Imported play file "${specifier}" must export definePlay(...) so it can be runtime-composed.`,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
importedPlayDependencies.set(resolved, {
|
|
825
|
+
filePath: resolved,
|
|
826
|
+
playName: importedPlayName,
|
|
827
|
+
});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
await visitFile(resolved);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (specifier.includes(':')) {
|
|
835
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
836
|
+
throw new Error(
|
|
837
|
+
`${absolutePath}:${position.line + 1}:${position.character + 1} Unsupported import specifier "${specifier}". Allowed imports are relative files, Node builtins, and installed packages.`,
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const packageImport = resolvePackageImport(specifier, absolutePath, adapter);
|
|
842
|
+
packages.set(packageImport.name, packageImport.version);
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const walk = async (node: ts.Node): Promise<void> => {
|
|
846
|
+
if (
|
|
847
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
848
|
+
node.moduleSpecifier &&
|
|
849
|
+
!(
|
|
850
|
+
(ts.isImportDeclaration(node) && node.importClause?.isTypeOnly) ||
|
|
851
|
+
(ts.isExportDeclaration(node) && node.isTypeOnly)
|
|
852
|
+
) &&
|
|
853
|
+
ts.isStringLiteralLike(node.moduleSpecifier)
|
|
854
|
+
) {
|
|
855
|
+
await handleSpecifier(node.moduleSpecifier.text, node, 'static');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (ts.isCallExpression(node)) {
|
|
859
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
860
|
+
if (node.arguments.length !== 1 || !ts.isStringLiteralLike(node.arguments[0]!)) {
|
|
861
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
862
|
+
throw new Error(
|
|
863
|
+
`${absolutePath}:${position.line + 1}:${position.character + 1} Dynamic import() is not allowed in plays. Use static imports instead.`,
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
await handleSpecifier(node.arguments[0]!.text, node, 'dynamic-import');
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
870
|
+
const firstArgument = node.arguments[0];
|
|
871
|
+
if (node.arguments.length !== 1 || !firstArgument || !ts.isStringLiteralLike(firstArgument)) {
|
|
872
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
873
|
+
throw new Error(
|
|
874
|
+
`${absolutePath}:${position.line + 1}:${position.character + 1} Dynamic require() is not allowed in plays. Use static imports or require(\"literal\") only.`,
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
await handleSpecifier(firstArgument.text, node, 'require');
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
for (const child of node.getChildren(sourceFile)) {
|
|
882
|
+
await walk(child);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
await walk(sourceFile);
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
await visitFile(absoluteEntryFile);
|
|
890
|
+
|
|
891
|
+
const sourceCode = localFiles.get(absoluteEntryFile) ?? '';
|
|
892
|
+
const sourceHash = sha256(sourceCode);
|
|
893
|
+
const graphHash = sha256(
|
|
894
|
+
JSON.stringify({
|
|
895
|
+
entryFile: absoluteEntryFile,
|
|
896
|
+
localFiles: [...localFiles.entries()]
|
|
897
|
+
.map(([filePath, contents]) => ({ filePath, hash: sha256(contents) }))
|
|
898
|
+
.sort((left, right) => left.filePath.localeCompare(right.filePath)),
|
|
899
|
+
nodeBuiltins: [...nodeBuiltins].sort(),
|
|
900
|
+
packages: [...packages.entries()]
|
|
901
|
+
.map(([name, version]) => ({ name, version }))
|
|
902
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
903
|
+
importedPlayDependencies: [...importedPlayDependencies.values()]
|
|
904
|
+
.map((dependency) => ({
|
|
905
|
+
filePath: dependency.filePath,
|
|
906
|
+
playName: dependency.playName,
|
|
907
|
+
}))
|
|
908
|
+
.sort((left, right) => left.filePath.localeCompare(right.filePath)),
|
|
909
|
+
}),
|
|
910
|
+
);
|
|
911
|
+
const playName = extractDefinedPlayName(sourceCode, absoluteEntryFile);
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
sourceCode,
|
|
915
|
+
sourceHash,
|
|
916
|
+
graphHash,
|
|
917
|
+
importPolicy: {
|
|
918
|
+
localFiles: [...localFiles.keys()].sort(),
|
|
919
|
+
nodeBuiltins: [...nodeBuiltins].sort(),
|
|
920
|
+
packages: [...packages.entries()]
|
|
921
|
+
.map(([name, version]) => ({ name, version }))
|
|
922
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
923
|
+
},
|
|
924
|
+
playName,
|
|
925
|
+
importedPlayDependencies: [...importedPlayDependencies.values()]
|
|
926
|
+
.sort((left, right) => left.filePath.localeCompare(right.filePath)),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Fingerprint of every TypeScript file in the Workers harness source dir.
|
|
932
|
+
* The harness gets bundled INTO every esm_workers play artifact, so any
|
|
933
|
+
* harness edit must invalidate the bundle cache and force a fresh CF
|
|
934
|
+
* deploy. Computed fresh on every bundle call so dev edits to entry.ts (or
|
|
935
|
+
* its peer DO files) are picked up on the next `play run` without
|
|
936
|
+
* restarting the dev server. (4 small files = sub-millisecond.) No
|
|
937
|
+
* caching: that's deliberate — caching the fingerprint is exactly the
|
|
938
|
+
* stale-state bug this exists to prevent.
|
|
939
|
+
*/
|
|
940
|
+
async function computeWorkersHarnessFingerprintWithAdapter(
|
|
941
|
+
adapter: PlayBundlingAdapter,
|
|
942
|
+
): Promise<string> {
|
|
943
|
+
const { readdir } = await import('node:fs/promises');
|
|
944
|
+
const entries = await readdir(adapter.workersHarnessFilesDir, { withFileTypes: true });
|
|
945
|
+
const tsFiles = entries
|
|
946
|
+
.filter((e) => e.isFile() && /\.[cm]?ts$/.test(e.name))
|
|
947
|
+
.map((e) => e.name)
|
|
948
|
+
.sort();
|
|
949
|
+
const parts: Array<{ name: string; hash: string }> = [];
|
|
950
|
+
for (const name of tsFiles) {
|
|
951
|
+
const contents = await readFile(join(adapter.workersHarnessFilesDir, name), 'utf-8');
|
|
952
|
+
parts.push({ name, hash: sha256(contents) });
|
|
953
|
+
}
|
|
954
|
+
return sha256(JSON.stringify(parts));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function artifactCachePath(
|
|
958
|
+
graphHash: string,
|
|
959
|
+
artifactKind: PlayArtifactKind,
|
|
960
|
+
adapter: PlayBundlingAdapter,
|
|
961
|
+
): string {
|
|
962
|
+
// Cache key includes artifactKind so a play can have both cjs_node20 and
|
|
963
|
+
// esm_workers builds cached side-by-side without one stomping on the other.
|
|
964
|
+
return join(
|
|
965
|
+
adapter.cacheDir ?? PLAY_ARTIFACT_CACHE_DIR,
|
|
966
|
+
`${graphHash}.${artifactKind}.json`,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function readArtifactCache(
|
|
971
|
+
graphHash: string,
|
|
972
|
+
artifactKind: PlayArtifactKind,
|
|
973
|
+
adapter: PlayBundlingAdapter,
|
|
974
|
+
): Promise<PlayBundleArtifact | null> {
|
|
975
|
+
try {
|
|
976
|
+
const serialized = await readFile(
|
|
977
|
+
artifactCachePath(graphHash, artifactKind, adapter),
|
|
978
|
+
'utf-8',
|
|
979
|
+
);
|
|
980
|
+
return JSON.parse(serialized) as PlayBundleArtifact;
|
|
981
|
+
} catch {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function writeArtifactCache(
|
|
987
|
+
artifact: PlayBundleArtifact,
|
|
988
|
+
adapter: PlayBundlingAdapter,
|
|
989
|
+
): Promise<void> {
|
|
990
|
+
const cacheDir = adapter.cacheDir ?? PLAY_ARTIFACT_CACHE_DIR;
|
|
991
|
+
await mkdir(cacheDir, { recursive: true });
|
|
992
|
+
await writeFile(
|
|
993
|
+
artifactCachePath(
|
|
994
|
+
artifact.graphHash,
|
|
995
|
+
artifact.artifactKind ?? PLAY_ARTIFACT_KINDS.cjsNode20,
|
|
996
|
+
adapter,
|
|
997
|
+
),
|
|
998
|
+
JSON.stringify(artifact),
|
|
999
|
+
'utf-8',
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function normalizeSourceMapForRuntime(sourceMapText: string): string {
|
|
1004
|
+
const parsed = JSON.parse(sourceMapText) as {
|
|
1005
|
+
sourceRoot?: string;
|
|
1006
|
+
sources?: string[];
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
parsed.sources = (parsed.sources ?? []).map((sourcePath) => {
|
|
1010
|
+
if (
|
|
1011
|
+
sourcePath.startsWith('data:') ||
|
|
1012
|
+
sourcePath.startsWith('node:') ||
|
|
1013
|
+
sourcePath.startsWith('/') ||
|
|
1014
|
+
/^[a-zA-Z]+:\/\//.test(sourcePath)
|
|
1015
|
+
) {
|
|
1016
|
+
return sourcePath;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return resolve(process.cwd(), sourcePath);
|
|
1020
|
+
});
|
|
1021
|
+
parsed.sourceRoot = undefined;
|
|
1022
|
+
|
|
1023
|
+
return JSON.stringify(parsed);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function getBundleSizeError(
|
|
1027
|
+
filePath: string,
|
|
1028
|
+
bundledCode: string,
|
|
1029
|
+
artifactKind: PlayArtifactKind,
|
|
1030
|
+
): string | null {
|
|
1031
|
+
const bundleBytes = Buffer.byteLength(bundledCode, 'utf8');
|
|
1032
|
+
if (bundleBytes > MAX_PLAY_BUNDLE_BYTES) {
|
|
1033
|
+
return `${filePath} Play bundle exceeds the 30 MiB limit (${bundleBytes} bytes > ${MAX_PLAY_BUNDLE_BYTES} bytes).`;
|
|
1034
|
+
}
|
|
1035
|
+
if (
|
|
1036
|
+
artifactKind === PLAY_ARTIFACT_KINDS.esmWorkers &&
|
|
1037
|
+
bundleBytes > MAX_ESM_WORKERS_BUNDLE_BYTES
|
|
1038
|
+
) {
|
|
1039
|
+
const mib = (bundleBytes / 1024 / 1024).toFixed(2);
|
|
1040
|
+
const limitMib = (MAX_ESM_WORKERS_BUNDLE_BYTES / 1024 / 1024).toFixed(2);
|
|
1041
|
+
return (
|
|
1042
|
+
`${filePath} Cloudflare Workers bundle is ${mib} MiB, above the workerd ` +
|
|
1043
|
+
`local-mode threshold of ${limitMib} MiB. Bundles past this size silently ` +
|
|
1044
|
+
`hang without executing the workflow body. Reduce dependencies (top ` +
|
|
1045
|
+
`offenders are usually date-fns, lodash, large schema libs) or split the ` +
|
|
1046
|
+
`play into smaller pieces with ctx.runPlay.`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export type BundlePlayFileOptions = {
|
|
1053
|
+
/**
|
|
1054
|
+
* Which artifact to produce. Defaults to `cjs_node20` (Daytona / local-process
|
|
1055
|
+
* runner). Pass `esm_workers` to produce a Cloudflare Worker bundle that
|
|
1056
|
+
* combines the play with the Workers harness.
|
|
1057
|
+
*
|
|
1058
|
+
* The CLI selects this from the execution profile or an explicit flag. Each
|
|
1059
|
+
* kind is cached independently on disk.
|
|
1060
|
+
*/
|
|
1061
|
+
target?: PlayArtifactKind;
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
export type BundlePlayFileCoreOptions = BundlePlayFileOptions & {
|
|
1065
|
+
adapter: PlayBundlingAdapter;
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
type EsbuildBundleOutput = {
|
|
1069
|
+
bundledCode: string;
|
|
1070
|
+
sourceMapText: string;
|
|
1071
|
+
outputExtension: 'cjs' | 'mjs';
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
async function runEsbuildForCjsNode(
|
|
1075
|
+
entryFile: string,
|
|
1076
|
+
importedPlayDependencies: ImportedPlayDependency[],
|
|
1077
|
+
adapter: PlayBundlingAdapter,
|
|
1078
|
+
): Promise<EsbuildBundleOutput | string[]> {
|
|
1079
|
+
const sdkAliasPlugin = localSdkAliasPlugin(adapter);
|
|
1080
|
+
const playProxyPlugin = importedPlayProxyPlugin(importedPlayDependencies);
|
|
1081
|
+
const result = await build({
|
|
1082
|
+
entryPoints: [entryFile],
|
|
1083
|
+
absWorkingDir: adapter.projectRoot,
|
|
1084
|
+
bundle: true,
|
|
1085
|
+
format: 'cjs',
|
|
1086
|
+
nodePaths: [adapter.nodeModulesDir],
|
|
1087
|
+
platform: 'node',
|
|
1088
|
+
target: ['node20'],
|
|
1089
|
+
outfile: 'play-artifact.cjs',
|
|
1090
|
+
write: false,
|
|
1091
|
+
sourcemap: 'external',
|
|
1092
|
+
sourcesContent: false,
|
|
1093
|
+
logLevel: 'silent',
|
|
1094
|
+
legalComments: 'none',
|
|
1095
|
+
plugins: [sdkAliasPlugin, playProxyPlugin].filter(
|
|
1096
|
+
(plugin): plugin is Plugin => plugin != null,
|
|
1097
|
+
),
|
|
1098
|
+
});
|
|
1099
|
+
const codeFile = result.outputFiles?.find((f) => f.path.endsWith('.cjs'));
|
|
1100
|
+
const mapFile = result.outputFiles?.find((f) => f.path.endsWith('.cjs.map'));
|
|
1101
|
+
if (!codeFile?.text || !mapFile?.text) {
|
|
1102
|
+
return ['Play bundling produced incomplete output.'];
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
bundledCode: codeFile.text,
|
|
1106
|
+
sourceMapText: mapFile.text,
|
|
1107
|
+
outputExtension: 'cjs',
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function runEsbuildForEsmWorkers(
|
|
1112
|
+
playEntryFile: string,
|
|
1113
|
+
importedPlayDependencies: ImportedPlayDependency[],
|
|
1114
|
+
adapter: PlayBundlingAdapter,
|
|
1115
|
+
): Promise<EsbuildBundleOutput | string[]> {
|
|
1116
|
+
const sdkAliasPlugin = localSdkAliasPlugin(adapter, { workersRuntime: true });
|
|
1117
|
+
const playProxyPlugin = importedPlayProxyPlugin(importedPlayDependencies);
|
|
1118
|
+
const playEntryAlias = workersPlayEntryAliasPlugin(playEntryFile);
|
|
1119
|
+
const result = await build({
|
|
1120
|
+
// Entry is the Workers harness; it imports the play via the virtual
|
|
1121
|
+
// `deepline-play-entry` alias resolved by workersPlayEntryAliasPlugin.
|
|
1122
|
+
entryPoints: [adapter.workersHarnessEntryFile],
|
|
1123
|
+
absWorkingDir: adapter.projectRoot,
|
|
1124
|
+
bundle: true,
|
|
1125
|
+
format: 'esm',
|
|
1126
|
+
nodePaths: [adapter.nodeModulesDir],
|
|
1127
|
+
// Browser platform with workerd + worker conditions matches Cloudflare's
|
|
1128
|
+
// V8-isolate runtime. Avoids accidentally pulling node-only branches of
|
|
1129
|
+
// dual-publish packages.
|
|
1130
|
+
platform: 'browser',
|
|
1131
|
+
conditions: ['workerd', 'worker', 'browser', 'import', 'default'],
|
|
1132
|
+
mainFields: ['module', 'main'],
|
|
1133
|
+
target: ['es2022'],
|
|
1134
|
+
outfile: 'play-worker.mjs',
|
|
1135
|
+
write: false,
|
|
1136
|
+
sourcemap: 'external',
|
|
1137
|
+
sourcesContent: false,
|
|
1138
|
+
logLevel: 'silent',
|
|
1139
|
+
legalComments: 'none',
|
|
1140
|
+
// Aggressive minify + treeshake: a tiny play (<1KB source) was producing
|
|
1141
|
+
// an ~870KB bundle, dominated by zod's locale files (~260KB unused) and
|
|
1142
|
+
// dead-code branches across `shared_libs/play-runtime/*` and the SDK.
|
|
1143
|
+
// Both DCE on with these flags. workerd compiles each unique-graphHash
|
|
1144
|
+
// bundle on first dispatch, so bundle bytes translate ~linearly into
|
|
1145
|
+
// per-play cold latency on workers_edge. External sourcemap remains
|
|
1146
|
+
// unchanged (`sourcemap: 'external'`), so debugging is unaffected.
|
|
1147
|
+
minify: true,
|
|
1148
|
+
treeShaking: true,
|
|
1149
|
+
// Most node:* builtins are provided by Cloudflare's `nodejs_compat` flag.
|
|
1150
|
+
// The unsupported subset (fs, fs/promises, os, child_process) gets
|
|
1151
|
+
// replaced with throwing stubs by workersNodeBuiltinStubPlugin so deploy
|
|
1152
|
+
// validation passes; anything supported (path, crypto, buffer, etc.) is
|
|
1153
|
+
// marked external and resolved at runtime by the Workers runtime.
|
|
1154
|
+
external: ['node:*', 'cloudflare:workers'],
|
|
1155
|
+
plugins: [
|
|
1156
|
+
// Banlist runs first so a forbidden import errors out before any other
|
|
1157
|
+
// resolver (esp. the workersNodeBuiltinStubPlugin which would silently
|
|
1158
|
+
// turn it into a throwing-stub).
|
|
1159
|
+
workersNodeImportBanlistPlugin(adapter),
|
|
1160
|
+
sdkAliasPlugin,
|
|
1161
|
+
playProxyPlugin,
|
|
1162
|
+
playEntryAlias,
|
|
1163
|
+
workersNodeBuiltinStubPlugin(),
|
|
1164
|
+
// Strip non-English zod locale data from the bundle. zod's locales
|
|
1165
|
+
// are re-exported as a static namespace from `zod/v4/core`, so
|
|
1166
|
+
// tree-shaking can't drop them; this plugin replaces each
|
|
1167
|
+
// non-English locale module with an empty default export at bundle
|
|
1168
|
+
// time. ~150–200 KB savings on every per-play bundle, with no
|
|
1169
|
+
// behavior change (we only surface English messages anyway).
|
|
1170
|
+
// If the bundle suddenly grows back, check whether a new dep added
|
|
1171
|
+
// its own zod copy or whether zod's locale layout changed.
|
|
1172
|
+
zodNonEnglishLocaleStubPlugin(),
|
|
1173
|
+
].filter((plugin): plugin is Plugin => plugin != null),
|
|
1174
|
+
});
|
|
1175
|
+
const codeFile = result.outputFiles?.find((f) => f.path.endsWith('.mjs'));
|
|
1176
|
+
const mapFile = result.outputFiles?.find((f) => f.path.endsWith('.mjs.map'));
|
|
1177
|
+
if (!codeFile?.text || !mapFile?.text) {
|
|
1178
|
+
return ['Workers play bundling produced incomplete output.'];
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
bundledCode: codeFile.text,
|
|
1182
|
+
sourceMapText: mapFile.text,
|
|
1183
|
+
outputExtension: 'mjs',
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export async function bundlePlayFile(
|
|
1188
|
+
filePath: string,
|
|
1189
|
+
options: BundlePlayFileCoreOptions,
|
|
1190
|
+
): Promise<BundledPlayFileResult> {
|
|
1191
|
+
const adapter = options.adapter;
|
|
1192
|
+
const target: PlayArtifactKind = options.target ?? PLAY_ARTIFACT_KINDS.cjsNode20;
|
|
1193
|
+
const absolutePath = await normalizeLocalPath(filePath);
|
|
1194
|
+
adapter.warnAboutNonDevelopmentBundling?.(absolutePath);
|
|
1195
|
+
|
|
1196
|
+
try {
|
|
1197
|
+
const analysis = await analyzeSourceGraph(absolutePath, adapter);
|
|
1198
|
+
// For esm_workers builds, the harness source files (entry.ts +
|
|
1199
|
+
// peer DO/coordinator types it imports) are bundled INTO every play
|
|
1200
|
+
// artifact. So any harness edit must produce a different graphHash so
|
|
1201
|
+
// (a) the bundle cache misses, (b) the per-graphHash CF Worker name
|
|
1202
|
+
// changes and gets a fresh deploy. This is the file-watcher-equivalent
|
|
1203
|
+
// for hot-reload of the harness: editing entry.ts → next play run
|
|
1204
|
+
// re-bundles + redeploys automatically.
|
|
1205
|
+
if (target === PLAY_ARTIFACT_KINDS.esmWorkers) {
|
|
1206
|
+
const harnessFingerprint = await computeWorkersHarnessFingerprintWithAdapter(adapter);
|
|
1207
|
+
analysis.graphHash = sha256(
|
|
1208
|
+
`${analysis.graphHash}\nworkers-harness:${harnessFingerprint}`,
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
// Cache lookup before typecheck/discovery: a cached artifact at this
|
|
1212
|
+
// graphHash is content-addressed by source — it can only exist if the
|
|
1213
|
+
// exact same source already passed typecheckPlaySource on a previous
|
|
1214
|
+
// run (we only write to cache after a successful bundle, which only
|
|
1215
|
+
// runs after a successful typecheck). Re-running ts.createProgram +
|
|
1216
|
+
// getPreEmitDiagnostics costs ~500ms per call and dominates warm
|
|
1217
|
+
// bundle latency, so skipping it on a cache hit is the single biggest
|
|
1218
|
+
// win for `play check` / `play run` startup time.
|
|
1219
|
+
const cachedArtifact = await readArtifactCache(analysis.graphHash, target, adapter);
|
|
1220
|
+
const discoveredFiles = await adapter.discoverPackagedLocalFiles(absolutePath);
|
|
1221
|
+
if (cachedArtifact) {
|
|
1222
|
+
const cachedArtifactSizeError = getBundleSizeError(
|
|
1223
|
+
absolutePath,
|
|
1224
|
+
cachedArtifact.bundledCode,
|
|
1225
|
+
target,
|
|
1226
|
+
);
|
|
1227
|
+
if (cachedArtifactSizeError) {
|
|
1228
|
+
return {
|
|
1229
|
+
success: false,
|
|
1230
|
+
filePath: absolutePath,
|
|
1231
|
+
errors: [cachedArtifactSizeError],
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return {
|
|
1236
|
+
success: true,
|
|
1237
|
+
artifact: { ...cachedArtifact, cacheHit: true },
|
|
1238
|
+
sourceCode: analysis.sourceCode,
|
|
1239
|
+
filePath: absolutePath,
|
|
1240
|
+
playName: analysis.playName,
|
|
1241
|
+
packagedFiles: discoveredFiles.files,
|
|
1242
|
+
unresolvedFileReferences: discoveredFiles.unresolved,
|
|
1243
|
+
importedPlayDependencies: analysis.importedPlayDependencies,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const typecheckErrors = [
|
|
1248
|
+
...(adapter.typecheckSdkTypes === false
|
|
1249
|
+
? []
|
|
1250
|
+
: typecheckPlaySource(analysis, adapter)),
|
|
1251
|
+
...((await adapter.typecheckPlaySource?.({
|
|
1252
|
+
sourceCode: analysis.sourceCode,
|
|
1253
|
+
sourcePath: absolutePath,
|
|
1254
|
+
importedFilePaths: [
|
|
1255
|
+
...analysis.importPolicy.localFiles,
|
|
1256
|
+
...analysis.importedPlayDependencies.map((dependency) => dependency.filePath),
|
|
1257
|
+
],
|
|
1258
|
+
})) ?? []),
|
|
1259
|
+
];
|
|
1260
|
+
if (typecheckErrors.length > 0) {
|
|
1261
|
+
return {
|
|
1262
|
+
success: false,
|
|
1263
|
+
filePath: absolutePath,
|
|
1264
|
+
errors: typecheckErrors,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
const buildOutcome =
|
|
1268
|
+
target === PLAY_ARTIFACT_KINDS.esmWorkers
|
|
1269
|
+
? await runEsbuildForEsmWorkers(absolutePath, analysis.importedPlayDependencies, adapter)
|
|
1270
|
+
: await runEsbuildForCjsNode(absolutePath, analysis.importedPlayDependencies, adapter);
|
|
1271
|
+
if (Array.isArray(buildOutcome)) {
|
|
1272
|
+
return {
|
|
1273
|
+
success: false,
|
|
1274
|
+
filePath: absolutePath,
|
|
1275
|
+
errors: buildOutcome,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
const { bundledCode, sourceMapText, outputExtension } = buildOutcome;
|
|
1279
|
+
|
|
1280
|
+
const normalizedSourceMap = normalizeSourceMapForRuntime(sourceMapText);
|
|
1281
|
+
const virtualFilename = `/virtual/deepline-plays/${analysis.graphHash}/${basename(absolutePath).replace(/\.[^.]+$/, '')}.${outputExtension}`;
|
|
1282
|
+
const executableCode = `${bundledCode}\n//# sourceMappingURL=${basename(virtualFilename)}.map\n`;
|
|
1283
|
+
const bundleSizeError = getBundleSizeError(
|
|
1284
|
+
absolutePath,
|
|
1285
|
+
executableCode,
|
|
1286
|
+
target,
|
|
1287
|
+
);
|
|
1288
|
+
if (bundleSizeError) {
|
|
1289
|
+
return {
|
|
1290
|
+
success: false,
|
|
1291
|
+
filePath: absolutePath,
|
|
1292
|
+
errors: [bundleSizeError],
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const codeFormat: PlayBundleArtifact['codeFormat'] =
|
|
1297
|
+
target === PLAY_ARTIFACT_KINDS.esmWorkers ? 'esm_module' : 'cjs_module';
|
|
1298
|
+
|
|
1299
|
+
const artifact: PlayBundleArtifact = {
|
|
1300
|
+
codeFormat,
|
|
1301
|
+
artifactKind: target,
|
|
1302
|
+
entryFile: absolutePath,
|
|
1303
|
+
virtualFilename,
|
|
1304
|
+
sourceHash: analysis.sourceHash,
|
|
1305
|
+
graphHash: analysis.graphHash,
|
|
1306
|
+
artifactHash: sha256(executableCode),
|
|
1307
|
+
sourceMapHash: sha256(normalizedSourceMap),
|
|
1308
|
+
bundledCode: executableCode,
|
|
1309
|
+
sourceMap: normalizedSourceMap,
|
|
1310
|
+
importPolicy: analysis.importPolicy,
|
|
1311
|
+
compatibility: buildPlayContractCompatibility(),
|
|
1312
|
+
generatedAt: Date.now(),
|
|
1313
|
+
cacheHit: false,
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
await writeArtifactCache(artifact, adapter);
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
success: true,
|
|
1320
|
+
artifact,
|
|
1321
|
+
sourceCode: analysis.sourceCode,
|
|
1322
|
+
filePath: absolutePath,
|
|
1323
|
+
playName: analysis.playName,
|
|
1324
|
+
packagedFiles: discoveredFiles.files,
|
|
1325
|
+
unresolvedFileReferences: discoveredFiles.unresolved,
|
|
1326
|
+
importedPlayDependencies: analysis.importedPlayDependencies,
|
|
1327
|
+
};
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
if (error && typeof error === 'object' && 'errors' in error) {
|
|
1330
|
+
const errors = Array.isArray((error as { errors?: Message[] }).errors)
|
|
1331
|
+
? ((error as { errors: Message[] }).errors).map(formatEsbuildMessage)
|
|
1332
|
+
: ['Play bundling failed.'];
|
|
1333
|
+
return {
|
|
1334
|
+
success: false,
|
|
1335
|
+
filePath: absolutePath,
|
|
1336
|
+
errors,
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return {
|
|
1341
|
+
success: false,
|
|
1342
|
+
filePath: absolutePath,
|
|
1343
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
}
|