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.
Files changed (100) hide show
  1. package/README.md +324 -0
  2. package/dist/cli/index.js +6750 -503
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/index.mjs +6735 -512
  5. package/dist/cli/index.mjs.map +1 -1
  6. package/dist/index.d.mts +2349 -32
  7. package/dist/index.d.ts +2349 -32
  8. package/dist/index.js +1631 -82
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +1617 -83
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +3256 -0
  13. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +710 -0
  14. package/dist/repo/apps/play-runner-workers/src/entry.ts +5070 -0
  15. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +21 -0
  16. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +177 -0
  17. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +52 -0
  18. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +100 -0
  19. package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +184 -0
  20. package/dist/repo/sdk/src/cli/commands/auth.ts +482 -0
  21. package/dist/repo/sdk/src/cli/commands/billing.ts +188 -0
  22. package/dist/repo/sdk/src/cli/commands/csv.ts +123 -0
  23. package/dist/repo/sdk/src/cli/commands/db.ts +119 -0
  24. package/dist/repo/sdk/src/cli/commands/feedback.ts +40 -0
  25. package/dist/repo/sdk/src/cli/commands/org.ts +117 -0
  26. package/dist/repo/sdk/src/cli/commands/play.ts +3200 -0
  27. package/dist/repo/sdk/src/cli/commands/tools.ts +687 -0
  28. package/dist/repo/sdk/src/cli/dataset-stats.ts +341 -0
  29. package/dist/repo/sdk/src/cli/index.ts +138 -0
  30. package/dist/repo/sdk/src/cli/progress.ts +135 -0
  31. package/dist/repo/sdk/src/cli/trace.ts +61 -0
  32. package/dist/repo/sdk/src/cli/utils.ts +145 -0
  33. package/dist/repo/sdk/src/client.ts +1188 -0
  34. package/dist/repo/sdk/src/compat.ts +77 -0
  35. package/dist/repo/sdk/src/config.ts +285 -0
  36. package/dist/repo/sdk/src/errors.ts +125 -0
  37. package/dist/repo/sdk/src/http.ts +391 -0
  38. package/dist/repo/sdk/src/index.ts +139 -0
  39. package/dist/repo/sdk/src/play.ts +1330 -0
  40. package/dist/repo/sdk/src/plays/bundle-play-file.ts +133 -0
  41. package/dist/repo/sdk/src/plays/harness-stub.ts +210 -0
  42. package/dist/repo/sdk/src/plays/local-file-discovery.ts +326 -0
  43. package/dist/repo/sdk/src/tool-output.ts +489 -0
  44. package/dist/repo/sdk/src/types.ts +669 -0
  45. package/dist/repo/sdk/src/version.ts +2 -0
  46. package/dist/repo/sdk/src/worker-play-entry.ts +286 -0
  47. package/dist/repo/shared_libs/observability/node-tracing.ts +129 -0
  48. package/dist/repo/shared_libs/observability/tracing.ts +98 -0
  49. package/dist/repo/shared_libs/play-runtime/backend.ts +139 -0
  50. package/dist/repo/shared_libs/play-runtime/batch-runtime.ts +182 -0
  51. package/dist/repo/shared_libs/play-runtime/batching-types.ts +91 -0
  52. package/dist/repo/shared_libs/play-runtime/context.ts +3999 -0
  53. package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +78 -0
  54. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +250 -0
  55. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +713 -0
  56. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +10 -0
  57. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +304 -0
  58. package/dist/repo/shared_libs/play-runtime/db-session.ts +462 -0
  59. package/dist/repo/shared_libs/play-runtime/dedup-backend.ts +0 -0
  60. package/dist/repo/shared_libs/play-runtime/default-batch-strategies.ts +124 -0
  61. package/dist/repo/shared_libs/play-runtime/execution-plan.ts +262 -0
  62. package/dist/repo/shared_libs/play-runtime/live-events.ts +214 -0
  63. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +50 -0
  64. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +114 -0
  65. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +158 -0
  66. package/dist/repo/shared_libs/play-runtime/profiles.ts +90 -0
  67. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +172 -0
  68. package/dist/repo/shared_libs/play-runtime/protocol.ts +121 -0
  69. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +42 -0
  70. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +33 -0
  71. package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +208 -0
  72. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +1873 -0
  73. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +2 -0
  74. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +201 -0
  75. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +48 -0
  76. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +84 -0
  77. package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +174 -0
  78. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +147 -0
  79. package/dist/repo/shared_libs/play-runtime/suspension.ts +68 -0
  80. package/dist/repo/shared_libs/play-runtime/tool-batch-executor.ts +146 -0
  81. package/dist/repo/shared_libs/play-runtime/tool-result.ts +387 -0
  82. package/dist/repo/shared_libs/play-runtime/tracing.ts +31 -0
  83. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +75 -0
  84. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +140 -0
  85. package/dist/repo/shared_libs/plays/artifact-transport.ts +14 -0
  86. package/dist/repo/shared_libs/plays/artifact-types.ts +49 -0
  87. package/dist/repo/shared_libs/plays/bundling/index.ts +1346 -0
  88. package/dist/repo/shared_libs/plays/compiler-manifest.ts +186 -0
  89. package/dist/repo/shared_libs/plays/contracts.ts +51 -0
  90. package/dist/repo/shared_libs/plays/dataset.ts +308 -0
  91. package/dist/repo/shared_libs/plays/definition.ts +264 -0
  92. package/dist/repo/shared_libs/plays/file-refs.ts +11 -0
  93. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +206 -0
  94. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +164 -0
  95. package/dist/repo/shared_libs/plays/row-identity.ts +302 -0
  96. package/dist/repo/shared_libs/plays/runtime-validation.ts +415 -0
  97. package/dist/repo/shared_libs/plays/static-pipeline.ts +560 -0
  98. package/dist/repo/shared_libs/temporal/constants.ts +39 -0
  99. package/dist/repo/shared_libs/temporal/preview-config.ts +153 -0
  100. 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
+ }