import { createHash } from 'node:crypto'; import { existsSync, readFileSync } from 'node:fs'; import { mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, } from 'node:path'; import { builtinModules } from 'node:module'; import { Parser } from 'acorn'; import { build, transformSync, type Loader, type Message, type Plugin, } from 'esbuild'; import { PLAY_ARTIFACT_KINDS, type PlayArtifactKind, } from '../../play-runtime/backend'; import type { PlayCompilerManifest } from '../compiler-manifest'; import type { PlayArtifactCompatibility, PlayBundleArtifact, PlayImportPolicy, PlayPackageImport, PlayRuntimeFeature, } from '../artifact-types'; import { buildPlayContractCompatibility } from '../contracts'; import { parsePlayDocflowFile, type PlayDocflowBinding } from '../docflow'; import { TypeScriptParser, astArray, astNodeBounds, buildDocflowParentIndex, docflowExpressionWrapsStepBuilder, docflowOutputRoot, findDocflowBoundStatement, isAstNode, resolveDocflowBinding, sourceLineStarts, type AstNode, } from '../docflow-binding'; export { collectDocflowBindingDrift, resolveDocflowBindingLines, resolveDocflowBindingSymbol, type DocflowBindingDriftDetail, type DocflowBindingResolution, type DocflowSymbolResolution, } from '../docflow-binding'; import type { ToolExecutionErrorSchemaVersion } from '../../tool-execution-error'; import type { PlaySandboxRuntimeDeclaration } from '../../play-runtime/sandbox-runtime-limits'; import { validatePlaySourceFilesHaveNoInlineSecrets } from '../secret-guardrails'; import { MAX_PLAY_BUNDLE_BYTES } from './limits'; import { PLAY_AUTHORING_CONTRACT_EDITION } from '../authoring-contract'; // The authored tool-error schema and the authored docflow block are both part // of the artifact bytes. Do not reuse a local bundle cached before either // compatibility selection or docflow instrumentation entered graph analysis. // Keep this aligned with the app and SDK adapters' cache namespace. const PLAY_BUNDLE_CACHE_VERSION = 36; const PLAY_ARTIFACT_CACHE_DIR = join( tmpdir(), `deepline-play-artifacts-v${PLAY_BUNDLE_CACHE_VERSION}`, ); const PLAY_PROXY_NAMESPACE = 'deepline-play-runtime-ref'; const SOURCE_EXTENSIONS = [ '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json', ]; const PLAY_SOURCE_FILE_PATTERN = /\.play\.(?:[cm]?[jt]sx?)$/i; const TEXT_IMPORT_EXTENSIONS = new Set(['.md', '.txt']); const NODE_BUILTIN_SET = new Set( builtinModules.flatMap((name) => name.startsWith('node:') ? [name, name.slice(5)] : [name, `node:${name}`], ), ); export type { PlayArtifactCompatibility, PlayBundleArtifact, PlayImportPolicy, PlayPackageImport, PlayRuntimeFeature, }; export type ImportedPlayDependency = { filePath: string; playName: string; }; export type PlayLocalFileReference = { sourceFragment: string; logicalPath: string; absolutePath: string; bytes: number; contentHash: string; contentType: string; }; export type PlayLocalFileDiscoveryError = { sourceFragment: string; message: string; }; export type PlayLocalFileDiscoveryResult = { files: PlayLocalFileReference[]; unresolved: PlayLocalFileDiscoveryError[]; }; export type PlayBundlingAdapter = { projectRoot: string; /** Optional root used only to make source-graph identity independent of temporary absolute paths. */ sourceIdentityRoot?: string; nodeModulesDir: string; cacheDir?: string; sdkSourceRoot: string; sdkPackageJson: string; sdkEntryFile: string; sdkTypesEntryFile?: string; discoverPackagedLocalFiles( filePath: string, ): Promise; typecheckPlaySource?(input: { sourceCode: string; sourcePath: string; importedFilePaths: string[]; }): Promise | string[]; warnAboutNonDevelopmentBundling?(filePath: string): void; }; type PathRelationshipApi = { relative(from: string, to: string): string; isAbsolute(path: string): boolean; sep: string; }; const defaultPathRelationshipApi: PathRelationshipApi = { relative, isAbsolute, sep, }; function assertValidExportName(exportName: string): void { if (exportName === 'default') return; if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(exportName)) { throw new Error( `Invalid play export name "${exportName}". Named prebuilt exports must be valid JavaScript identifiers.`, ); } } export type BundledPlayFileSuccess = { success: true; artifact: PlayBundleArtifact; sourceCode: string; sourceFiles: Record; filePath: string; playName: string | null; playDescription: string | null; sandboxRuntimeDeclaration: PlaySandboxRuntimeDeclaration | null; compilerManifest?: PlayCompilerManifest; packagedFiles: PlayLocalFileReference[]; unresolvedFileReferences: PlayLocalFileDiscoveryError[]; importedPlayDependencies: ImportedPlayDependency[]; }; export type BundledPlayFileFailure = { success: false; errors: string[]; filePath: string; }; export type BundledPlayFileResult = | BundledPlayFileSuccess | BundledPlayFileFailure; type PackageResolution = { name: string; version: string | null; }; type SourceGraphAnalysis = { sourceCode: string; sourceFiles: Record; sourceHash: string; graphHash: string; importPolicy: PlayImportPolicy; playName: string | null; playDescription: string | null; sandboxRuntimeDeclaration: PlaySandboxRuntimeDeclaration | null; toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion | null; toolResponseReceiptRevision: string | null; importedPlayDependencies: ImportedPlayDependency[]; }; type PlayWorkspace = { entryFile: string; rootDir: string; }; type SourceImportReference = { specifier: string; line: number; column: number; kind: 'static' | 'require' | 'dynamic-import'; }; function sha256(value: string): string { return createHash('sha256').update(value).digest('hex'); } function sourceIdentityPath( filePath: string, sourceIdentityRoot: string | undefined, ): string { if (!sourceIdentityRoot) return filePath; const identityRoot = resolve(sourceIdentityRoot); const resolvedFilePath = resolve(filePath); const logicalPath = relative(identityRoot, resolvedFilePath); if (!logicalPath) { return basename(resolvedFilePath); } if ( logicalPath === '..' || logicalPath.startsWith(`..${sep}`) || isAbsolute(logicalPath) ) { return filePath; } return logicalPath.split(/[\\/]+/).join('/'); } function formatEsbuildMessage(message: Message): string { const location = message.location ? `${message.location.file}:${message.location.line}:${message.location.column}` : null; return location ? `${location} ${message.text}` : message.text; } function isLocalSpecifier(specifier: string): boolean { return ( specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/') || specifier.startsWith('file:') ); } async function normalizeLocalPath(filePath: string): Promise { try { return await realpath(filePath); } catch { return resolve(filePath); } } function createPlayWorkspace(entryFile: string): PlayWorkspace { return { entryFile, rootDir: dirname(entryFile), }; } export function isPathInsideDirectory( filePath: string, directory: string, pathApi: PathRelationshipApi = defaultPathRelationshipApi, ): boolean { const relationship = pathApi.relative(directory, filePath); return ( relationship === '' || (relationship !== '..' && !relationship.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relationship)) ); } function assertWithinPlayWorkspace(input: { importer: string; specifier: string; resolvedPath: string; workspace: PlayWorkspace; line: number; column: number; }): void { if (isPathInsideDirectory(input.resolvedPath, input.workspace.rootDir)) { return; } throw new Error( `${input.importer}:${input.line}:${input.column} ` + `Local play imports must stay inside the play workspace (${input.workspace.rootDir}). ` + `Import "${input.specifier}" resolved to ${input.resolvedPath}, which crosses into app/backend code. ` + 'Use the public SDK/API surface or move shared helpers into the play workspace.', ); } function getPackageName(specifier: string): string { if (specifier.startsWith('@')) { const [scope, name] = specifier.split('/'); return scope && name ? `${scope}/${name}` : specifier; } return specifier.split('/')[0] ?? specifier; } function isPlaySourceFile(filePath: string): boolean { return PLAY_SOURCE_FILE_PATTERN.test(filePath); } function stripCommentsToSpaces(source: string): string { return source .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' ')) .replace( /(^|[^:])\/\/.*$/gm, (match, prefix: string) => prefix + ' '.repeat(Math.max(0, match.length - prefix.length)), ); } function previousSignificantToken(source: string, index: number): string { let cursor = index - 1; while (cursor >= 0 && /\s/.test(source[cursor]!)) cursor -= 1; if (cursor < 0) return ''; const char = source[cursor]!; if (!/[A-Za-z0-9_$]/.test(char)) return char; let start = cursor; while (start > 0 && /[A-Za-z0-9_$]/.test(source[start - 1]!)) start -= 1; return source.slice(start, cursor + 1); } function canStartRegexLiteral(source: string, slashIndex: number): boolean { const token = previousSignificantToken(source, slashIndex); return ( !token || token === 'return' || token === 'throw' || token === 'case' || token === 'delete' || token === 'typeof' || token === 'void' || token === 'instanceof' || /^(?:[({[=,:;!&|?+\-*~^<>%]|\.\.\.)$/.test(token) ); } function maskSourceForStructure(source: string): string { const chars = source.split(''); const maskRange = (start: number, end: number) => { for (let cursor = start; cursor < end; cursor += 1) { if (chars[cursor] !== '\n') chars[cursor] = ' '; } }; for (let index = 0; index < source.length; index += 1) { const char = source[index]!; const next = source[index + 1]; if (char === '/' && next === '/') { let end = index + 2; while (end < source.length && source[end] !== '\n') end += 1; maskRange(index, end); index = end; continue; } if (char === '/' && next === '*') { const close = source.indexOf('*/', index + 2); const end = close >= 0 ? close + 2 : source.length; maskRange(index, end); index = end - 1; continue; } if (char === '"' || char === "'" || char === '`') { const quote = char; let end = index + 1; let escaped = false; while (end < source.length) { const current = source[end]!; if (escaped) { escaped = false; } else if (current === '\\') { escaped = true; } else if (current === quote) { end += 1; break; } end += 1; } maskRange(index, end); index = end - 1; continue; } if (char === '/' && canStartRegexLiteral(source, index)) { let end = index + 1; let escaped = false; let inCharacterClass = false; while (end < source.length) { const current = source[end]!; if (escaped) { escaped = false; } else if (current === '\\') { escaped = true; } else if (current === '[') { inCharacterClass = true; } else if (current === ']') { inCharacterClass = false; } else if (current === '/' && !inCharacterClass) { end += 1; while (/[A-Za-z]/.test(source[end] ?? '')) end += 1; break; } else if (current === '\n') { break; } end += 1; } maskRange(index, end); index = end - 1; } } return chars.join(''); } function lineAndColumnAt( source: string, index: number, ): { line: number; column: number } { const prefix = source.slice(0, index); const lines = prefix.split('\n'); return { line: lines.length, column: lines[lines.length - 1]!.length + 1 }; } function findSourceImportReferences( sourceCode: string, ): SourceImportReference[] { const source = stripCommentsToSpaces(sourceCode); const references: SourceImportReference[] = []; const addReference = ( specifier: string | undefined, specifierIndex: number, kind: SourceImportReference['kind'], ) => { if (!specifier) return; const position = lineAndColumnAt(sourceCode, specifierIndex); references.push({ specifier, line: position.line, column: position.column, kind, }); }; const staticImportPattern = /\b(?:import|export)\s+(?!type\b)(?:[\s\S]*?\s+from\s*)?(['"])([^'"\n]+)\1/g; for (const match of source.matchAll(staticImportPattern)) { addReference( match[2], match.index! + match[0].lastIndexOf(match[1]!), 'static', ); } const dynamicImportPattern = /\bimport\s*\(\s*(['"])([^'"\n]+)\1/g; for (const match of source.matchAll(dynamicImportPattern)) { addReference( match[2], match.index! + match[0].lastIndexOf(match[1]!), 'dynamic-import', ); } const requirePattern = /\brequire\s*\(\s*(['"])([^'"\n]+)\1/g; for (const match of source.matchAll(requirePattern)) { addReference( match[2], match.index! + match[0].lastIndexOf(match[1]!), 'require', ); } const literalDynamicImportIndexes = new Set( [...source.matchAll(dynamicImportPattern)].map((match) => match.index!), ); for (const match of source.matchAll(/\bimport\s*\(/g)) { if (literalDynamicImportIndexes.has(match.index!)) continue; const position = lineAndColumnAt(sourceCode, match.index!); throw new Error( `:${position.line}:${position.column} Dynamic import() is not allowed in plays. Use static imports instead.`, ); } const literalRequireIndexes = new Set( [...source.matchAll(requirePattern)].map((match) => match.index!), ); for (const match of source.matchAll(/\brequire\s*\(/g)) { if (literalRequireIndexes.has(match.index!)) continue; const position = lineAndColumnAt(sourceCode, match.index!); throw new Error( `:${position.line}:${position.column} Dynamic require() is not allowed in plays. Use static imports or require("literal") only.`, ); } return references.sort((left, right) => left.line === right.line ? left.column - right.column : left.line - right.line, ); } /** * Reports local runtime edges using the same parser that drives graph analysis. * Migration code uses this to distinguish truly single-file source from stored * graph metadata without maintaining a second import parser. */ export function countLocalRuntimeImportReferences(sourceCode: string): number { return findSourceImportReferences(sourceCode).filter((reference) => isLocalSpecifier(reference.specifier), ).length; } function findMatchingBrace(source: string, openIndex: number): number { const structuralSource = maskSourceForStructure(source); let depth = 0; for (let index = openIndex; index < structuralSource.length; index += 1) { const char = structuralSource[index]!; if (char === '{') depth += 1; if (char === '}') { depth -= 1; if (depth === 0) return index; } } return -1; } export function extractDefinedPlayName(sourceCode: string): string | null { return extractDefinedPlayMetadata(sourceCode, null)?.name ?? null; } export function extractDefinedPlayNameForExport( sourceCode: string, exportName: string, ): string | null { return extractDefinedPlayMetadata(sourceCode, exportName)?.name ?? null; } export function extractDefinedPlayDescription( sourceCode: string, ): string | null { return extractDefinedPlayMetadata(sourceCode, null)?.description ?? null; } export function extractDefinedPlayDescriptionForExport( sourceCode: string, exportName: string, ): string | null { return ( extractDefinedPlayMetadata(sourceCode, exportName)?.description ?? null ); } type PlayMetadataExtractionContext = { declarations: Map; namedExports: Map; commonJsExports: Map; defaultExport: AstNode | null; }; type ExtractedPlayMetadata = { name: string | null; description: string | null; sandboxRuntimeDeclaration: PlaySandboxRuntimeDeclaration | null; toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion | null; toolErrorSchemaVersionUnknown: boolean; toolResponseReceiptRevision: string | null; toolResponseReceiptRevisionUnknown: boolean; }; function parsePlaySourceAst(sourceCode: string): AstNode | null { try { return TypeScriptParser.parse(sourceCode, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true, }) as unknown as AstNode; } catch { try { const transformed = transformSync(sourceCode, { loader: 'ts', format: 'esm', target: 'esnext', legalComments: 'none', sourcemap: false, }).code; return Parser.parse(transformed, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true, }) as unknown as AstNode; } catch { return null; } } } function getIdentifierName(node: unknown): string | null { return isAstNode(node) && node.type === 'Identifier' ? typeof node.name === 'string' ? node.name : null : null; } function memberExpressionPath( node: AstNode | null | undefined, ): string[] | null { const expression = unwrapStaticExpression(node); if (!expression) return null; if (expression.type === 'Identifier' && typeof expression.name === 'string') { return [expression.name]; } if (expression.type !== 'MemberExpression') return null; const objectPath = memberExpressionPath( isAstNode(expression.object) ? expression.object : null, ); if (!objectPath) return null; const property = isAstNode(expression.property) ? expression.property : null; if (!property) return null; if (!expression.computed && property.type === 'Identifier') { return typeof property.name === 'string' ? [...objectPath, property.name] : null; } if (expression.computed && property.type === 'Literal') { return typeof property.value === 'string' ? [...objectPath, property.value] : null; } return null; } function commonJsExportName(left: AstNode | null | undefined): string | null { const path = memberExpressionPath(left); if (!path) return null; if (path.length === 2 && path[0] === 'module' && path[1] === 'exports') { return 'default'; } if (path.length === 3 && path[0] === 'module' && path[1] === 'exports') { return path[2] || null; } if (path.length === 2 && path[0] === 'exports') { return path[1] || null; } return null; } function isDefinePlayCallExpression(node: AstNode | null | undefined): boolean { if (!node || node.type !== 'CallExpression') return false; const callee = isAstNode(node.callee) ? node.callee : null; if (!callee) return false; if (callee.type === 'Identifier') { return callee.name === 'definePlay' || callee.name === 'defineWorkflow'; } if (callee.type === 'MemberExpression' && isAstNode(callee.property)) { const property = callee.property; return ( !callee.computed && property.type === 'Identifier' && (property.name === 'definePlay' || property.name === 'defineWorkflow') ); } return false; } function canResolveDeclarationInitializer( declarationKind: unknown, initializer: AstNode | null, ): boolean { return declarationKind === 'const' || isDefinePlayCallExpression(initializer); } function buildPlayMetadataContext(ast: AstNode): PlayMetadataExtractionContext { const declarations = new Map(); const namedExports = new Map(); const commonJsExports = new Map(); let defaultExport: AstNode | null = null; for (const statement of astArray(ast.body)) { if (statement.type === 'VariableDeclaration') { for (const declaration of astArray(statement.declarations)) { const name = getIdentifierName(declaration.id); if (!name) continue; const initializer = isAstNode(declaration.init) ? declaration.init : null; declarations.set( name, canResolveDeclarationInitializer(statement.kind, initializer) ? initializer : null, ); } continue; } if (statement.type === 'ExportDefaultDeclaration') { defaultExport = isAstNode(statement.declaration) ? statement.declaration : null; continue; } if (statement.type === 'TSExportAssignment') { defaultExport = isAstNode(statement.expression) ? statement.expression : null; continue; } if (statement.type === 'ExportNamedDeclaration') { if ( isAstNode(statement.declaration) && statement.declaration.type === 'VariableDeclaration' ) { for (const declaration of astArray( statement.declaration.declarations, )) { const name = getIdentifierName(declaration.id); if (!name) continue; const initializer = isAstNode(declaration.init) ? declaration.init : null; declarations.set( name, canResolveDeclarationInitializer( statement.declaration.kind, initializer, ) ? initializer : null, ); namedExports.set(name, name); } } for (const specifier of astArray(statement.specifiers)) { const localName = getIdentifierName(specifier.local); const exportedName = getIdentifierName(specifier.exported) ?? (isAstNode(specifier.exported) && specifier.exported.type === 'Literal' && typeof specifier.exported.value === 'string' ? specifier.exported.value : null); if (localName && exportedName) { namedExports.set(exportedName, localName); } } } if ( statement.type === 'ExpressionStatement' && isAstNode(statement.expression) && statement.expression.type === 'AssignmentExpression' ) { const exportName = commonJsExportName( isAstNode(statement.expression.left) ? statement.expression.left : null, ); if (exportName && isAstNode(statement.expression.right)) { commonJsExports.set(exportName, statement.expression.right); } } } return { declarations, namedExports, commonJsExports, defaultExport }; } function unwrapStaticExpression( node: AstNode | null | undefined, ): AstNode | null { let current = node ?? null; while ( current && (current.type === 'TSAsExpression' || current.type === 'TSSatisfiesExpression' || current.type === 'TSTypeAssertion' || current.type === 'TSNonNullExpression' || current.type === 'ParenthesizedExpression') ) { current = isAstNode(current.expression) ? current.expression : null; } return current; } function staticStringFromExpression( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, seen = new Set(), trimResult = true, ): string | null { const expression = unwrapStaticExpression(node); if (!expression) return null; if (expression.type === 'Literal') { const value = expression.value; if (typeof value !== 'string' || !value.trim()) return null; return trimResult ? value.trim() : value; } if ( expression.type === 'TemplateLiteral' && astArray(expression.expressions).length === 0 ) { const firstQuasi = astArray(expression.quasis)[0]; const cooked = firstQuasi && typeof firstQuasi.value === 'object' ? (firstQuasi.value as { cooked?: unknown }).cooked : null; if (typeof cooked !== 'string' || !cooked.trim()) return null; return trimResult ? cooked.trim() : cooked; } if (expression.type === 'BinaryExpression' && expression.operator === '+') { const left = staticStringFromExpression( isAstNode(expression.left) ? expression.left : null, context, new Set(seen), false, ); const right = staticStringFromExpression( isAstNode(expression.right) ? expression.right : null, context, new Set(seen), false, ); const value = left !== null && right !== null ? `${left}${right}` : null; if (!value?.trim()) return null; return trimResult ? value.trim() : value; } const identifier = getIdentifierName(expression); if (!identifier || seen.has(identifier)) return null; seen.add(identifier); return staticStringFromExpression( context.declarations.get(identifier), context, seen, trimResult, ); } function propertyNameFromKey(property: AstNode): string | null { const key = isAstNode(property.key) ? property.key : null; if (!key) return null; if (property.computed) { return key.type === 'Literal' && typeof key.value === 'string' ? key.value : null; } if (key.type === 'Identifier' && typeof key.name === 'string') { return key.name; } if (key.type === 'Literal' && typeof key.value === 'string') { return key.value; } return null; } function objectExpressionFromNode( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, seen = new Set(), ): AstNode | null { const expression = unwrapStaticExpression(node); if (!expression) return null; if (expression.type === 'ObjectExpression') return expression; const identifier = getIdentifierName(expression); if (!identifier || seen.has(identifier)) return null; seen.add(identifier); return objectExpressionFromNode( context.declarations.get(identifier), context, seen, ); } type StaticPropertyResolution = | { kind: 'absent' } | { kind: 'found'; value: AstNode } | { kind: 'unknown' }; function resolveStaticProperty( node: AstNode | null | undefined, propertyName: string, context: PlayMetadataExtractionContext, ancestors = new Set(), ): StaticPropertyResolution { if (!node) return { kind: 'absent' }; const object = objectExpressionFromNode(node, context); if (!object) return { kind: 'unknown' }; if (ancestors.has(object)) return { kind: 'unknown' }; const nextAncestors = new Set(ancestors).add(object); let resolution: StaticPropertyResolution = { kind: 'absent' }; for (const property of astArray(object.properties)) { if (property.type === 'SpreadElement') { const spreadResolution = resolveStaticProperty( isAstNode(property.argument) ? property.argument : null, propertyName, context, nextAncestors, ); if (spreadResolution.kind !== 'absent') { resolution = spreadResolution; } continue; } if (property.type !== 'Property') continue; const resolvedPropertyName = propertyNameFromKey(property); if (resolvedPropertyName !== propertyName) { if (property.computed && resolvedPropertyName === null) { resolution = { kind: 'unknown' }; } continue; } const propertyValue = property.shorthand ? property.key : property.value; resolution = isAstNode(propertyValue) ? { kind: 'found', value: propertyValue } : { kind: 'unknown' }; } return resolution; } function staticPropertyNames( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, ancestors = new Set(), ): Set | null { if (!node) return new Set(); const object = objectExpressionFromNode(node, context); if (!object || ancestors.has(object)) return null; const nextAncestors = new Set(ancestors).add(object); const names = new Set(); for (const property of astArray(object.properties)) { if (property.type === 'SpreadElement') { const spreadNames = staticPropertyNames( isAstNode(property.argument) ? property.argument : null, context, nextAncestors, ); if (!spreadNames) return null; for (const name of spreadNames) names.add(name); continue; } if (property.type !== 'Property') return null; const name = propertyNameFromKey(property); if (name === null) return null; names.add(name); } return names; } function staticNumberFromExpression( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, seen = new Set(), ): number | null { const expression = unwrapStaticExpression(node); if (!expression) return null; if (expression.type === 'Literal' && typeof expression.value === 'number') { return expression.value; } const identifier = getIdentifierName(expression); if (!identifier || seen.has(identifier)) return null; seen.add(identifier); return staticNumberFromExpression( context.declarations.get(identifier), context, seen, ); } function toolErrorSchemaVersionFromOptions( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, ): ToolExecutionErrorSchemaVersion | null | undefined { const directCompatibility = resolveStaticProperty( node, 'compatibility', context, ); if (directCompatibility.kind === 'unknown') return null; const bindings = resolveStaticProperty(node, 'bindings', context); if (bindings.kind === 'unknown') return null; const bindingCompatibility = bindings.kind === 'found' ? resolveStaticProperty(bindings.value, 'compatibility', context) : ({ kind: 'absent' } satisfies StaticPropertyResolution); if (bindingCompatibility.kind === 'unknown') return null; const compatibility = directCompatibility.kind === 'found' ? directCompatibility : bindingCompatibility; if (compatibility.kind === 'absent') return undefined; const schema = resolveStaticProperty( compatibility.value, 'toolErrorSchemaVersion', context, ); if (schema.kind === 'absent') return undefined; if (schema.kind !== 'found') return null; const schemaVersion = staticNumberFromExpression(schema.value, context); if (schemaVersion === 0 || schemaVersion === 1) return schemaVersion; return null; } function toolResponseReceiptRevisionFromOptions( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, ): string | null | undefined { const directCompatibility = resolveStaticProperty( node, 'compatibility', context, ); if (directCompatibility.kind === 'unknown') return null; const bindings = resolveStaticProperty(node, 'bindings', context); if (bindings.kind === 'unknown') return null; const bindingCompatibility = bindings.kind === 'found' ? resolveStaticProperty(bindings.value, 'compatibility', context) : ({ kind: 'absent' } satisfies StaticPropertyResolution); if (bindingCompatibility.kind === 'unknown') return null; const compatibility = directCompatibility.kind === 'found' ? directCompatibility : bindingCompatibility; if (compatibility.kind === 'absent') return undefined; const revision = resolveStaticProperty( compatibility.value, 'toolResponseReceiptRevision', context, ); if (revision.kind === 'absent') return undefined; if (revision.kind !== 'found') return null; return staticStringFromExpression(revision.value, context); } function sandboxRuntimeDeclarationFromOptions( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, ): PlaySandboxRuntimeDeclaration | null { const directRuntime = resolveStaticProperty(node, 'runtime', context); const bindings = resolveStaticProperty(node, 'bindings', context); const bindingRuntime = bindings.kind === 'found' ? resolveStaticProperty(bindings.value, 'runtime', context) : ({ kind: 'absent' } satisfies StaticPropertyResolution); const runtime = directRuntime.kind === 'found' ? directRuntime : bindingRuntime; if (runtime.kind !== 'found') return null; const propertyNames = staticPropertyNames(runtime.value, context); const unsupportedProperties = propertyNames ? [...propertyNames].filter( (propertyName) => propertyName !== 'timeout' && propertyName !== 'size', ) : []; if (unsupportedProperties.length > 0) { throw new Error( `Unsupported runtime sandbox option${unsupportedProperties.length === 1 ? '' : 's'} "${unsupportedProperties.join('", "')}". ` + 'Use runtime.size with a Deepline prebuilt sandbox. Supported sizes: "standard".', ); } const timeout = resolveStaticProperty(runtime.value, 'timeout', context); const size = resolveStaticProperty(runtime.value, 'size', context); const declaration: PlaySandboxRuntimeDeclaration = {}; if (timeout.kind === 'found') { const value = staticStringFromExpression(timeout.value, context); if (value === null) return null; declaration.timeout = value; } if (size.kind === 'found') { const value = staticStringFromExpression(size.value, context); if (value === null) return null; declaration.size = value as PlaySandboxRuntimeDeclaration['size']; } return declaration; } function stringPropertyFromObjectExpression( node: AstNode | null | undefined, propertyName: string, context: PlayMetadataExtractionContext, seenObjects = new Set(), ): string | null { const object = objectExpressionFromNode(node, context); if (!object || seenObjects.has(object)) return null; seenObjects.add(object); let value: string | null = null; for (const property of astArray(object.properties)) { if (property.type === 'SpreadElement') { const spreadValue = stringPropertyFromObjectExpression( isAstNode(property.argument) ? property.argument : null, propertyName, context, seenObjects, ); if (spreadValue) value = spreadValue; continue; } if (property.type !== 'Property') continue; if (propertyNameFromKey(property) !== propertyName) continue; const propertyValue = property.shorthand ? property.key : property.value; value = staticStringFromExpression( isAstNode(propertyValue) ? propertyValue : null, context, ); } return value; } function isDefinePlayCallee(node: AstNode | null | undefined): boolean { const callee = unwrapStaticExpression(node); if (!callee) return false; if ( callee.type === 'Identifier' && (callee.name === 'definePlay' || callee.name === 'defineWorkflow') ) { return true; } if (callee.type === 'MemberExpression' && isAstNode(callee.property)) { const property = callee.property; return ( !callee.computed && property.type === 'Identifier' && (property.name === 'definePlay' || property.name === 'defineWorkflow') ); } return false; } function playMetadataFromDefinePlayCall( node: AstNode | null | undefined, context: PlayMetadataExtractionContext, ): ExtractedPlayMetadata | null { const expression = unwrapStaticExpression(node); if ( !expression || expression.type !== 'CallExpression' || !isDefinePlayCallee(isAstNode(expression.callee) ? expression.callee : null) ) { return null; } const args = astArray(expression.arguments); const firstArg = args[0] ?? null; const isObjectForm = args.length === 1 || objectExpressionFromNode(firstArg, context) !== null; const objectName = stringPropertyFromObjectExpression( firstArg, 'id', context, ); const name = isObjectForm ? objectName : staticStringFromExpression(firstArg, context); let description = stringPropertyFromObjectExpression(firstArg, 'description', context) ?? null; for (let index = args.length - 1; !description && index >= 2; index -= 1) { description = stringPropertyFromObjectExpression( args[index], 'description', context, ); } const options = isObjectForm ? firstArg : (args[2] ?? null); const sandboxRuntimeDeclaration = sandboxRuntimeDeclarationFromOptions( options, context, ); const toolErrorSchemaVersion = toolErrorSchemaVersionFromOptions( options, context, ); const toolErrorSchemaVersionUnknown = toolErrorSchemaVersion === null; const toolResponseReceiptRevision = toolResponseReceiptRevisionFromOptions( options, context, ); const toolResponseReceiptRevisionUnknown = toolResponseReceiptRevision === null; if ( !name && !description && toolErrorSchemaVersion === undefined && !toolErrorSchemaVersionUnknown && toolResponseReceiptRevision === undefined && !toolResponseReceiptRevisionUnknown ) { return null; } return { name, description, sandboxRuntimeDeclaration, toolErrorSchemaVersion: toolErrorSchemaVersion ?? null, toolErrorSchemaVersionUnknown, toolResponseReceiptRevision: toolResponseReceiptRevision ?? null, toolResponseReceiptRevisionUnknown, }; } function resolveExportExpression( exportName: string | null, context: PlayMetadataExtractionContext, ): AstNode | null { if (exportName === 'default') { const commonJsDefault = context.commonJsExports.get('default'); if (commonJsDefault) { const commonJsDefaultIdentifier = getIdentifierName( unwrapStaticExpression(commonJsDefault), ); return commonJsDefaultIdentifier ? (context.declarations.get(commonJsDefaultIdentifier) ?? commonJsDefault) : commonJsDefault; } const defaultExpression = unwrapStaticExpression(context.defaultExport); const defaultIdentifier = getIdentifierName(defaultExpression); if (!defaultIdentifier) { const defaultExportName = context.namedExports.get('default'); return defaultExportName ? (context.declarations.get(defaultExportName) ?? null) : defaultExpression; } return defaultIdentifier ? (context.declarations.get(defaultIdentifier) ?? context.defaultExport) : context.defaultExport; } if (exportName) { const commonJsExport = context.commonJsExports.get(exportName); if (commonJsExport) { const commonJsExportIdentifier = getIdentifierName( unwrapStaticExpression(commonJsExport), ); return commonJsExportIdentifier ? (context.declarations.get(commonJsExportIdentifier) ?? commonJsExport) : commonJsExport; } const localName = context.namedExports.get(exportName) ?? exportName; return context.declarations.get(localName) ?? null; } return null; } function extractDefinedPlayMetadata( sourceCode: string, exportName: string | null, ): ExtractedPlayMetadata | null { const ast = parsePlaySourceAst(sourceCode); if (!ast) return null; const context = buildPlayMetadataContext(ast); const exportedExpression = resolveExportExpression(exportName, context); const exportedMetadata = playMetadataFromDefinePlayCall( exportedExpression, context, ); if (exportedMetadata) return exportedMetadata; if (exportName) return null; if (context.defaultExport) { const directDefaultMetadata = playMetadataFromDefinePlayCall( context.defaultExport, context, ); if (directDefaultMetadata) return directDefaultMetadata; const defaultIdentifier = getIdentifierName(context.defaultExport); if (defaultIdentifier) { const defaultMetadata = playMetadataFromDefinePlayCall( context.declarations.get(defaultIdentifier), context, ); if (defaultMetadata) return defaultMetadata; } } for (const expression of context.declarations.values()) { const metadata = playMetadataFromDefinePlayCall(expression, context); if (metadata) return metadata; } for (const expression of context.commonJsExports.values()) { const metadata = playMetadataFromDefinePlayCall(expression, context); if (metadata) return metadata; } return null; } function readPackageVersionFromPackageJson( packageJsonPath: string, packageName: string, ): string | null { try { const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { name?: unknown; version?: unknown; }; if ( packageJson.name === packageName && typeof packageJson.version === 'string' ) { return packageJson.version; } } catch { return null; } return null; } function findPackageJsonPathFrom( startDir: string, packageName: string, ): string | null { if (!isAbsolute(startDir)) { throw new Error( `Package resolution requires an absolute start directory, got ${startDir}`, ); } let current = startDir; while (true) { const packageJsonPath = join( current, 'node_modules', packageName, 'package.json', ); if (existsSync(packageJsonPath)) { return packageJsonPath; } const parent = dirname(current); if (parent === current) { return null; } current = parent; } } function findPackageJsonPath( packageName: string, fromFile: string, adapter: PlayBundlingAdapter, ): string | null { const startDirs = [ resolve(dirname(fromFile)), resolve(adapter.projectRoot), resolve(dirname(adapter.sdkPackageJson)), ]; const seen = new Set(); for (const startDir of startDirs) { if (seen.has(startDir)) continue; seen.add(startDir); const packageJsonPath = findPackageJsonPathFrom(startDir, packageName); if (packageJsonPath) return packageJsonPath; } const adapterNodeModulesPackageJson = join( adapter.nodeModulesDir, packageName, 'package.json', ); return existsSync(adapterNodeModulesPackageJson) ? adapterNodeModulesPackageJson : null; } function localSdkAliasPlugin(adapter: PlayBundlingAdapter): Plugin | null { const entryFile = adapter.sdkEntryFile; if (!existsSync(entryFile)) { return null; } return { name: 'deepline-sdk-local-alias', setup(buildContext) { buildContext.onResolve({ filter: /^deepline$/ }, () => ({ path: entryFile, })); buildContext.onResolve({ filter: /^deepline\/helpers$/ }, () => ({ path: join(adapter.sdkSourceRoot, 'helpers.ts'), })); }, }; } function isTextImportFile(path: string): boolean { return TEXT_IMPORT_EXTENSIONS.has(extname(path)); } function sourceLoaderForPath(path: string): Loader { const rawExtension = extname(path); if (TEXT_IMPORT_EXTENSIONS.has(rawExtension)) return 'text'; const extension = rawExtension.toLowerCase(); if (extension === '.tsx') return 'tsx'; if (extension === '.jsx') return 'jsx'; if (extension === '.js' || extension === '.mjs' || extension === '.cjs') { return 'js'; } return 'ts'; } function docflowContextIdentifierFromCall(call: AstNode): string | null { if (!isDefinePlayCallExpression(call)) return null; const args = astArray(call.arguments); const callback = args.find( (argument) => argument.type === 'ArrowFunctionExpression' || argument.type === 'FunctionExpression', ); if (!callback) return null; return getIdentifierName(astArray(callback.params)[0]); } /** * Finds the context parameter for the definePlay call that owns one binding. * A file may export several plays with different parameter names, so a single * file-global context identifier is not safe for instrumentation. */ function findDocflowContextIdentifier(input: { ast: AstNode; statement: AstNode | null; bindingLine: number; lineStarts: readonly number[]; parents: ReadonlyMap; }): string | null { let owner: AstNode | null = input.statement; while (owner) { const contextName = docflowContextIdentifierFromCall(owner); if (contextName) return contextName; owner = input.parents.get(owner) ?? null; } // Positional instrumentation can survive an AST resolver abstention. In that // case, choose the smallest definePlay call containing the bound line. const lineStart = input.lineStarts[input.bindingLine - 1]; const lineEnd = input.lineStarts[input.bindingLine] ?? Number.POSITIVE_INFINITY; if (lineStart === undefined) return null; let best: AstNode | null = null; const pending: unknown[] = [input.ast]; while (pending.length > 0) { const value = pending.pop(); if (!isAstNode(value)) continue; if (isDefinePlayCallExpression(value)) { const bounds = astNodeBounds(value); if ( bounds && bounds.start < lineEnd && bounds.end > lineStart && (!best || bounds.end - bounds.start < (astNodeBounds(best)?.end ?? 0) - (astNodeBounds(best)?.start ?? 0)) ) { best = value; } } for (const child of Object.values(value)) { if (Array.isArray(child)) pending.push(...child); else if (isAstNode(child)) pending.push(child); } } return best ? docflowContextIdentifierFromCall(best) : null; } type DocflowInstrumentationReplacement = { start: number; end: number; value: string; }; function docflowInputCaptureSource(paths: readonly string[]): string { return `[${paths .map((path) => { const [root, ...properties] = path.split('.'); return `{path:${JSON.stringify(path)},readRoot:()=>${root},properties:${JSON.stringify(properties)}}`; }) .join(',')}]`; } /** The expression span a bound statement exposes for runtime observation. */ function docflowObservationTarget( statement: ReturnType, ): { start: number; end: number; awaitsResult: boolean; expression: AstNode; } | null { if (!statement) return null; let expression: AstNode | null = null; if (statement.type === 'VariableDeclaration') { const declarations = astArray(statement.declarations); if (declarations.length === 1 && isAstNode(declarations[0]!.init)) { expression = declarations[0]!.init; } } else if ( statement.type === 'ReturnStatement' && isAstNode(statement.argument) ) { expression = statement.argument; } else if ( statement.type === 'ExpressionStatement' && isAstNode(statement.expression) ) { expression = statement.expression; } else if (statement.type === 'IfStatement' && isAstNode(statement.test)) { expression = statement.test; } if (!expression) return null; const bounds = astNodeBounds(expression); if (!bounds) return null; return { ...bounds, awaitsResult: expressionContainsTopLevelAwait(expression), expression, }; } function docflowObservedExpression(input: { contextName: string; nodeId: string; inputs: readonly string[]; outputs: readonly string[]; expression: string; awaitsResult: boolean; }): string { const callback = input.awaitsResult ? `async()=>(${input.expression})` : `()=>(${input.expression})`; const observation = `((runtimeContext,inputCaptures,execute)=>typeof runtimeContext.__deeplineObserveDocflowNode==="function"?runtimeContext.__deeplineObserveDocflowNode(${JSON.stringify(input.nodeId)},inputCaptures,${JSON.stringify(input.outputs)},execute):execute())(${input.contextName},${docflowInputCaptureSource(input.inputs)},${callback})`; return input.awaitsResult ? `await ${observation}` : observation; } function expressionContainsTopLevelAwait(expression: AstNode): boolean { const pending: AstNode[] = [expression]; while (pending.length > 0) { const node = pending.pop()!; if (node.type === 'AwaitExpression') return true; if ( node !== expression && (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') ) { continue; } for (const child of Object.values(node)) { if (Array.isArray(child)) pending.push(...child.filter(isAstNode)); else if (isAstNode(child)) pending.push(child); } } return false; } /** * Instruments authored nodes at AST expression boundaries. Declarations, * expressions, decisions, and returns are wrapped so the runtime reports the * actual result or failure. Rewrites remain single-line so source mappings and * the executable program's block scope do not change. */ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string { // EVERY block's bindings, not just the export being bundled. Two exports' // statements are disjoint, so instrumenting both is correct for whichever one // runs, and the esbuild plugin never has to learn which export it is building. const parsed = parsePlayDocflowFile(sourceCode); if (parsed.blocks.length === 0 || parsed.errors.length > 0) return sourceCode; const ast = parsePlaySourceAst(sourceCode); if (!ast) return sourceCode; const lineStarts = sourceLineStarts(sourceCode); const replacements: DocflowInstrumentationReplacement[] = []; // Several annotations may bind one statement — stacked above the dataset // call, or on a chained `.withColumn(...)` line INSIDE it, where the bound // target is an inner expression whose span overlaps (not equals) the // statement's. Overlapping text replacements corrupt the rewritten source, // so every set of overlapping spans collapses into ONE group at the // outermost span, emitted as a single nested observation. type ObservationTarget = { start: number; end: number; awaitsResult: boolean; contextName: string; binding: PlayDocflowBinding; }; const targets: ObservationTarget[] = []; // Build the parent index once so every binding's symbol resolution reuses it. const parents = buildDocflowParentIndex(ast); for (const binding of parsed.bindings) { // ADR 0016 rule 1: prefer the statement the `out:` symbol names; fall back // to positional resolution. Instrumentation and `plays check` share this // resolver, so the observed statement and the lint agree. const resolution = resolveDocflowBinding(ast, binding, lineStarts, parents); const contextName = findDocflowContextIdentifier({ ast, statement: resolution.statement, bindingLine: binding.line, lineStarts, parents, }); if (!contextName) continue; const target = docflowObservationTarget(resolution.statement); // A binding naming a `.step('', …)` leg of a waterfall is NOT // instrumented, and the omission is deliberate rather than a gap. // // A leg's enclosing statement is the whole `steps().step(…).step(…)` builder: // one expression, shared by every leg, which runs once and synchronously to // produce a program object. Wrapping it would report the BUILDER's // construction — instantly settled, with the program as its "output" — under // each leg's node id, and every leg in the cascade would observe the same // span. That is not partial truth, it is a false one, and the canvas would // render a row of completed steps before a single provider had been called. // // Observing the leg itself would mean a new rewrite shape wrapping each // step's RESOLVER, which runs per row inside a dataset — one observation row // per leg per row, against ADR 0016's bound that maps observe at the node // level because per-row truth already lives in sheets. So a waterfall region // is observation-free by construction: the canvas suppresses run state on leg // members and marks only the answering leg, read from the cascade's own // durable result. // // The test is the SHAPE of the observed expression, not how the binding // resolved: every shipped prebuilt builds its cascade in a module-level // helper, which the enclosing-`definePlay` symbol scope deliberately cannot // see, so those legs resolve positionally and would otherwise slip through. const observedRoot = docflowOutputRoot(binding); if ( target && observedRoot && docflowExpressionWrapsStepBuilder(target.expression, observedRoot) ) { continue; } if (target) { targets.push({ ...target, contextName, binding }); continue; } const lineStart = lineStarts[binding.line - 1]; if (lineStart !== undefined) { const indentationLength = /^\s*/.exec(sourceCode.slice(lineStart))?.[0].length ?? 0; replacements.push({ start: lineStart + indentationLength, end: lineStart + indentationLength, value: `await (typeof ${contextName}.__deeplineDocflowHit==="function"?${contextName}.__deeplineDocflowHit(${JSON.stringify(binding.nodeId)}):void 0); `, }); } } // AST spans nest, so after sorting (container first: earliest start, then // longest) any overlapping target is contained by the open group. The sort // is stable, so same-span bindings keep their annotation order. targets.sort( (left, right) => left.start - right.start || right.end - left.end, ); type ObservationGroup = { start: number; end: number; awaitsResult: boolean; contextName: string; bindings: PlayDocflowBinding[]; }; const groups: ObservationGroup[] = []; for (const target of targets) { const open = groups[groups.length - 1]; if (open && target.start < open.end) { open.bindings.push(target.binding); } else { groups.push({ start: target.start, end: target.end, awaitsResult: target.awaitsResult, contextName: target.contextName, bindings: [target.binding], }); } } for (const group of groups) { // Wrap in reverse order so the first-listed annotation observes outermost. let expression = sourceCode.slice(group.start, group.end); for (const binding of [...group.bindings].reverse()) { expression = docflowObservedExpression({ contextName: group.contextName, nodeId: binding.nodeId, inputs: binding.inputs ?? [], outputs: binding.outputs ?? [], expression, awaitsResult: group.awaitsResult, }); } replacements.push({ start: group.start, end: group.end, value: expression, }); } return replacements .sort((left, right) => right.start - left.start) .reduce( (source, replacement) => `${source.slice(0, replacement.start)}${replacement.value}${source.slice(replacement.end)}`, sourceCode, ); } function docflowRuntimeInstrumentationPlugin( customerSourceFilePaths: readonly string[], ): Plugin { const customerSourceFiles = new Set( customerSourceFilePaths .filter((path) => extname(path).toLowerCase() !== '.json') .map((path) => resolve(path)), ); return { name: 'deepline-docflow-runtime-instrumentation', setup(buildContext) { buildContext.onLoad({ filter: /./ }, (args) => { if (!customerSourceFiles.has(resolve(args.path))) return undefined; if (isTextImportFile(args.path)) { return { contents: readFileSync(args.path, 'utf8'), loader: 'text', resolveDir: dirname(args.path), }; } return { contents: instrumentPlayDocflowRuntimeHits( readFileSync(args.path, 'utf8'), ), loader: sourceLoaderForPath(args.path), resolveDir: dirname(args.path), }; }); }, }; } function buildImportedPlayProxyModule(playName: string): string { const serializedName = JSON.stringify(playName); return ` const PLAY_METADATA_SYMBOL = Symbol.for('deepline.play.metadata'); const importedPlayRef = async function importedPlayRef(ctx, input) { return ctx.runPlay(${JSON.stringify(`imported_${playName.replace(/[^A-Za-z0-9_]+/g, '_')}`)}, importedPlayRef, input, { description: 'Run the imported Deepline play dependency.', }); }; Object.defineProperty(importedPlayRef, 'playName', { value: ${serializedName}, enumerable: true, configurable: false, writable: false, }); Object.defineProperty(importedPlayRef, PLAY_METADATA_SYMBOL, { value: { name: ${serializedName} }, enumerable: false, configurable: false, writable: false, }); export const playName = ${serializedName}; export const name = ${serializedName}; export default importedPlayRef; `; } function importedPlayProxyPlugin( importedPlayDependencies: ImportedPlayDependency[], ): Plugin | null { if (importedPlayDependencies.length === 0) { return null; } const dependenciesByPath = new Map( importedPlayDependencies.map((dependency) => [ dependency.filePath, dependency, ]), ); return { name: 'deepline-imported-play-proxy', setup(buildContext) { buildContext.onResolve({ filter: /.*/ }, async (args) => { if (!args.importer || !isLocalSpecifier(args.path)) { return null; } const resolvedPath = await resolveLocalImport(args.importer, args.path); const dependency = dependenciesByPath.get(resolvedPath); if (!dependency) { return null; } return { path: dependency.filePath, namespace: PLAY_PROXY_NAMESPACE, pluginData: dependency, }; }); buildContext.onLoad( { filter: /.*/, namespace: PLAY_PROXY_NAMESPACE }, async (args) => { const dependency = (args.pluginData as ImportedPlayDependency | undefined) ?? dependenciesByPath.get(args.path); if (!dependency) { return null; } return { contents: buildImportedPlayProxyModule(dependency.playName), loader: 'ts', resolveDir: dirname(args.path), }; }, ); }, }; } async function fileExists(filePath: string): Promise { try { await stat(filePath); return true; } catch { return false; } } async function resolveLocalImport( fromFile: string, specifier: string, ): Promise { if (specifier.startsWith('file:')) { return normalizeLocalPath(new URL(specifier).pathname); } const base = isAbsolute(specifier) ? resolve(specifier) : resolve(dirname(fromFile), specifier); const candidates: string[] = [base]; const explicitExtension = extname(base).toLowerCase(); if (!explicitExtension) { candidates.push( ...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`), ); candidates.push( ...SOURCE_EXTENSIONS.map((extension) => join(base, `index${extension}`)), ); } else if (['.js', '.jsx', '.mjs', '.cjs'].includes(explicitExtension)) { const stem = base.slice(0, -explicitExtension.length); candidates.push( ...SOURCE_EXTENSIONS.map((extension) => `${stem}${extension}`), ); } for (const candidate of candidates) { if (await fileExists(candidate)) { return normalizeLocalPath(candidate); } } throw new Error( `Could not resolve local import "${specifier}" from ${fromFile}`, ); } function resolvePackageImport( specifier: string, fromFile: string, adapter: PlayBundlingAdapter, ): PackageResolution { const packageName = getPackageName(specifier); if (packageName === 'deepline' && existsSync(adapter.sdkPackageJson)) { const packageJson = JSON.parse( readFileSync(adapter.sdkPackageJson, 'utf-8'), ) as { version?: string }; return { name: 'deepline', version: packageJson.version ?? null, }; } const packageJsonPath = findPackageJsonPath(packageName, fromFile, adapter); if (!packageJsonPath) { throw new Error(`Could not resolve "${specifier}" from ${fromFile}`); } return { name: packageName, version: readPackageVersionFromPackageJson(packageJsonPath, packageName), }; } async function analyzeSourceGraph( entryFile: string, adapter: PlayBundlingAdapter, exportName: string, ): Promise { const absoluteEntryFile = await normalizeLocalPath(entryFile); const workspace = createPlayWorkspace(absoluteEntryFile); const sourceIdentityRoot = adapter.sourceIdentityRoot; const localFiles = new Map(); const nodeBuiltins = new Set(); const packages = new Map(); const importedPlayDependencies = new Map(); const visited = new Set(); const visitFile = async (filePath: string) => { const absolutePath = await normalizeLocalPath(filePath); if (visited.has(absolutePath)) { return; } visited.add(absolutePath); const sourceCode = await readFile( /* turbopackIgnore: true */ absolutePath, 'utf-8', ); localFiles.set(absolutePath, sourceCode); if ( extname(absolutePath).toLowerCase() === '.json' || isTextImportFile(absolutePath) ) { return; } const handleSpecifier = async ( specifier: string, line: number, column: number, kind: 'static' | 'require' | 'dynamic-import', ) => { if (kind === 'dynamic-import') { throw new Error( `${absolutePath}:${line}:${column} Dynamic import() is not allowed in plays. Use static imports instead.`, ); } if (NODE_BUILTIN_SET.has(specifier)) { nodeBuiltins.add( specifier.startsWith('node:') ? specifier : `node:${specifier}`, ); return; } if (isLocalSpecifier(specifier)) { const resolved = await resolveLocalImport(absolutePath, specifier); assertWithinPlayWorkspace({ importer: absolutePath, specifier, resolvedPath: resolved, workspace, line, column, }); if (resolved !== absoluteEntryFile && isPlaySourceFile(resolved)) { const importedSource = await readFile( /* turbopackIgnore: true */ resolved, 'utf-8', ); const importedPlayName = extractDefinedPlayName(importedSource); if (!importedPlayName) { throw new Error( `${absolutePath}:${line}:${column} Imported play file "${specifier}" must export definePlay(...) so it can be runtime-composed.`, ); } importedPlayDependencies.set(resolved, { filePath: resolved, playName: importedPlayName, }); return; } await visitFile(resolved); return; } if (specifier.includes(':')) { throw new Error( `${absolutePath}:${line}:${column} Unsupported import specifier "${specifier}". Allowed imports are relative files, Node builtins, and installed packages.`, ); } const packageImport = resolvePackageImport( specifier, absolutePath, adapter, ); packages.set(packageImport.name, packageImport.version); }; try { for (const reference of findSourceImportReferences(sourceCode)) { await handleSpecifier( reference.specifier, reference.line, reference.column, reference.kind, ); } } catch (error) { if (error instanceof Error && error.message.startsWith(':')) { throw new Error(`${absolutePath}${error.message}`); } throw error; } }; await visitFile(absoluteEntryFile); const sourceCode = localFiles.get(absoluteEntryFile) ?? ''; const sourceHash = sha256(sourceCode); const graphHash = sha256( JSON.stringify({ entryFile: sourceIdentityPath(absoluteEntryFile, sourceIdentityRoot), localFiles: [...localFiles.entries()] .map(([filePath, contents]) => ({ filePath: sourceIdentityPath(filePath, sourceIdentityRoot), hash: sha256(contents), })) .sort((left, right) => left.filePath.localeCompare(right.filePath)), nodeBuiltins: [...nodeBuiltins].sort(), packages: [...packages.entries()] .map(([name, version]) => ({ name, version })) .sort((left, right) => left.name.localeCompare(right.name)), importedPlayDependencies: [...importedPlayDependencies.values()] .map((dependency) => ({ filePath: sourceIdentityPath(dependency.filePath, sourceIdentityRoot), playName: dependency.playName, })) .sort((left, right) => left.filePath.localeCompare(right.filePath)), }), ); const metadata = extractDefinedPlayMetadata(sourceCode, exportName) ?? (exportName === 'default' ? extractDefinedPlayMetadata(sourceCode, null) : null); if (metadata?.toolErrorSchemaVersionUnknown) { throw new Error( 'definePlay compatibility.toolErrorSchemaVersion must be the static literal 0 or 1.', ); } if (metadata?.toolResponseReceiptRevisionUnknown) { throw new Error( 'definePlay compatibility.toolResponseReceiptRevision must be a static non-empty string.', ); } const playName = metadata?.name ?? null; const playDescription = metadata?.description ?? null; const sandboxRuntimeDeclaration = metadata?.sandboxRuntimeDeclaration ?? null; return { sourceCode, sourceFiles: Object.fromEntries( [...localFiles.entries()].sort((left, right) => left[0].localeCompare(right[0]), ), ), sourceHash, graphHash, importPolicy: { localFiles: [...localFiles.keys()].sort(), nodeBuiltins: [...nodeBuiltins].sort(), packages: [...packages.entries()] .map(([name, version]) => ({ name, version })) .sort((left, right) => left.name.localeCompare(right.name)), }, playName, playDescription, sandboxRuntimeDeclaration, toolErrorSchemaVersion: metadata?.toolErrorSchemaVersion ?? null, toolResponseReceiptRevision: metadata?.toolResponseReceiptRevision ?? null, importedPlayDependencies: [...importedPlayDependencies.values()].sort( (left, right) => left.filePath.localeCompare(right.filePath), ), }; } function artifactCachePath( graphHash: string, artifactKind: PlayArtifactKind, adapter: PlayBundlingAdapter, ): string { return join( /* turbopackIgnore: true */ adapter.cacheDir ?? PLAY_ARTIFACT_CACHE_DIR, `${graphHash}.${artifactKind}.json`, ); } async function readArtifactCache( graphHash: string, artifactKind: PlayArtifactKind, adapter: PlayBundlingAdapter, ): Promise { try { const serialized = await readFile( /* turbopackIgnore: true */ artifactCachePath( graphHash, artifactKind, adapter, ), 'utf-8', ); return JSON.parse(serialized) as PlayBundleArtifact; } catch { return null; } } async function writeArtifactCache( artifact: PlayBundleArtifact, adapter: PlayBundlingAdapter, ): Promise { const cacheDir = adapter.cacheDir ?? PLAY_ARTIFACT_CACHE_DIR; await mkdir(/* turbopackIgnore: true */ cacheDir, { recursive: true }); await writeFile( /* turbopackIgnore: true */ artifactCachePath( artifact.graphHash, artifact.artifactKind ?? PLAY_ARTIFACT_KINDS.cjsNode20, adapter, ), JSON.stringify(artifact), 'utf-8', ); } function normalizeSourceMapForRuntime( sourceMapText: string, projectRoot: string, ): string { const parsed = JSON.parse(sourceMapText) as { sourceRoot?: string; sources?: string[]; }; parsed.sources = (parsed.sources ?? []).map((sourcePath) => { if ( sourcePath.startsWith('data:') || sourcePath.startsWith('node:') || sourcePath.startsWith('/') || /^[a-zA-Z]+:\/\//.test(sourcePath) ) { return sourcePath; } return join(projectRoot, sourcePath); }); parsed.sourceRoot = undefined; return JSON.stringify(parsed); } export function getBundleSizeErrorForBytes( filePath: string, bundleBytes: number, _artifactKind: PlayArtifactKind, ): string | null { if (bundleBytes > MAX_PLAY_BUNDLE_BYTES) { return `${filePath} Play bundle exceeds the 30 MiB limit (${bundleBytes} bytes > ${MAX_PLAY_BUNDLE_BYTES} bytes).`; } return null; } function getBundleSizeError( filePath: string, bundledCode: string, artifactKind: PlayArtifactKind, ): string | null { return getBundleSizeErrorForBytes( filePath, Buffer.byteLength(bundledCode, 'utf8'), artifactKind, ); } export type BundlePlayFileOptions = { /** Must be `cjs_node20`; retained as an explicit compile-contract assertion. */ target?: PlayArtifactKind; exportName?: string; }; export type BundlePlayFileCoreOptions = BundlePlayFileOptions & { adapter: PlayBundlingAdapter; }; type EsbuildBundleOutput = { bundledCode: string; sourceMapText: string; outputExtension: 'cjs'; }; async function runEsbuildForCjsNode( entryFile: string, customerSourceFilePaths: string[], importedPlayDependencies: ImportedPlayDependency[], adapter: PlayBundlingAdapter, exportName: string, ): Promise { const sdkAliasPlugin = localSdkAliasPlugin(adapter); const playProxyPlugin = importedPlayProxyPlugin(importedPlayDependencies); const docflowPlugin = docflowRuntimeInstrumentationPlugin([ entryFile, ...customerSourceFilePaths, ]); const namedExportShim = exportName === 'default' ? null : `export { ${exportName} as default } from ${JSON.stringify(entryFile)};\n`; const result = await build({ ...(namedExportShim ? { stdin: { contents: namedExportShim, resolveDir: dirname(entryFile), sourcefile: `${basename(entryFile)}.${exportName}.entry.ts`, loader: 'ts' as const, }, } : { entryPoints: [entryFile] }), absWorkingDir: adapter.projectRoot, bundle: true, format: 'cjs', nodePaths: [adapter.nodeModulesDir], platform: 'node', target: ['node20'], outfile: 'play-artifact.cjs', write: false, sourcemap: 'external', sourcesContent: false, logLevel: 'silent', legalComments: 'none', plugins: [docflowPlugin, sdkAliasPlugin, playProxyPlugin].filter( (plugin): plugin is Plugin => plugin != null, ), }); const codeFile = result.outputFiles?.find((f) => f.path.endsWith('.cjs')); const mapFile = result.outputFiles?.find((f) => f.path.endsWith('.cjs.map')); if (!codeFile?.text || !mapFile?.text) { return ['Play bundling produced incomplete output.']; } return { bundledCode: codeFile.text, sourceMapText: mapFile.text, outputExtension: 'cjs', }; } export async function bundlePlayFile( filePath: string, options: BundlePlayFileCoreOptions, ): Promise { const adapter = options.adapter; const target: PlayArtifactKind = options.target ?? PLAY_ARTIFACT_KINDS.cjsNode20; const exportName = options.exportName?.trim() || 'default'; assertValidExportName(exportName); const absolutePath = await normalizeLocalPath(filePath); // Keep the existing absolute-path wire format unless a caller explicitly // opts into a logical identity root. Production's deployed checker still // uses absolute paths to reconcile bundled imports; source export normalizes // those paths into a portable tree at the API boundary. const sourceIdentityRoot = adapter.sourceIdentityRoot; adapter.warnAboutNonDevelopmentBundling?.(absolutePath); try { const analysis = await analyzeSourceGraph( absolutePath, adapter, exportName, ); analysis.graphHash = sha256( `${analysis.graphHash}\nentry-export:${exportName}\nauthoring-contract-edition:${PLAY_AUTHORING_CONTRACT_EDITION}`, ); const sourceFiles = Object.fromEntries( Object.entries(analysis.sourceFiles).map(([sourcePath, sourceCode]) => [ sourceIdentityPath(sourcePath, sourceIdentityRoot), sourceCode, ]), ); const importPolicy: PlayImportPolicy = { ...analysis.importPolicy, localFiles: analysis.importPolicy.localFiles.map((sourcePath) => sourceIdentityPath(sourcePath, sourceIdentityRoot), ), }; const entryFile = sourceIdentityPath(absolutePath, sourceIdentityRoot); try { validatePlaySourceFilesHaveNoInlineSecrets(analysis.sourceFiles); } catch (error) { return { success: false, filePath: absolutePath, errors: [error instanceof Error ? error.message : String(error)], }; } const typecheckErrors = [ ...((await adapter.typecheckPlaySource?.({ sourceCode: analysis.sourceCode, sourcePath: absolutePath, importedFilePaths: [ ...analysis.importPolicy.localFiles, ...analysis.importedPlayDependencies.map( (dependency) => dependency.filePath, ), ], })) ?? []), ]; if (typecheckErrors.length > 0) { return { success: false, filePath: absolutePath, errors: typecheckErrors, }; } // Cache lookup happens after validation because a bundle cache hit is keyed // by source and target, while cloud descriptor typecheck results also depend // on generated tool metadata. const cachedArtifact = await readArtifactCache( analysis.graphHash, target, adapter, ); const discoveredFiles = await adapter.discoverPackagedLocalFiles(absolutePath); if (cachedArtifact) { const cachedArtifactSizeError = getBundleSizeError( absolutePath, cachedArtifact.bundledCode, target, ); if (cachedArtifactSizeError) { return { success: false, filePath: absolutePath, errors: [cachedArtifactSizeError], }; } return { success: true, artifact: { ...cachedArtifact, entryFile, sourceHash: analysis.sourceHash, importPolicy, compatibility: buildPlayContractCompatibility({ toolErrorSchemaVersion: analysis.toolErrorSchemaVersion ?? undefined, toolResponseReceiptRevision: analysis.toolResponseReceiptRevision ?? undefined, }), cacheHit: true, }, sourceCode: analysis.sourceCode, sourceFiles, filePath: absolutePath, playName: analysis.playName, playDescription: analysis.playDescription, sandboxRuntimeDeclaration: analysis.sandboxRuntimeDeclaration, packagedFiles: discoveredFiles.files, unresolvedFileReferences: discoveredFiles.unresolved, importedPlayDependencies: analysis.importedPlayDependencies, }; } const buildOutcome = await runEsbuildForCjsNode( absolutePath, analysis.importPolicy.localFiles, analysis.importedPlayDependencies, adapter, exportName, ); if (Array.isArray(buildOutcome)) { return { success: false, filePath: absolutePath, errors: buildOutcome, }; } const { bundledCode, sourceMapText, outputExtension } = buildOutcome; const normalizedSourceMap = normalizeSourceMapForRuntime( sourceMapText, resolve(adapter.projectRoot), ); const virtualBaseName = exportName === 'default' ? basename(absolutePath).replace(/\.[^.]+$/, '') : `${basename(absolutePath).replace(/\.[^.]+$/, '')}.${exportName}`; const virtualFilename = `/virtual/deepline-plays/${analysis.graphHash}/${virtualBaseName}.${outputExtension}`; const executableCode = `${bundledCode}\n//# sourceMappingURL=${basename(virtualFilename)}.map\n`; const bundleSizeError = getBundleSizeError( absolutePath, executableCode, target, ); if (bundleSizeError) { return { success: false, filePath: absolutePath, errors: [bundleSizeError], }; } const artifact: PlayBundleArtifact = { codeFormat: 'cjs_module', artifactKind: target, entryFile, virtualFilename, sourceHash: analysis.sourceHash, graphHash: analysis.graphHash, artifactHash: sha256(executableCode), sourceMapHash: sha256(normalizedSourceMap), bundledCode: executableCode, sourceMap: normalizedSourceMap, importPolicy, compatibility: buildPlayContractCompatibility({ toolErrorSchemaVersion: analysis.toolErrorSchemaVersion ?? undefined, toolResponseReceiptRevision: analysis.toolResponseReceiptRevision ?? undefined, }), generatedAt: Date.now(), cacheHit: false, }; await writeArtifactCache(artifact, adapter); return { success: true, artifact, sourceCode: analysis.sourceCode, sourceFiles, filePath: absolutePath, playName: analysis.playName, playDescription: analysis.playDescription, sandboxRuntimeDeclaration: analysis.sandboxRuntimeDeclaration, packagedFiles: discoveredFiles.files, unresolvedFileReferences: discoveredFiles.unresolved, importedPlayDependencies: analysis.importedPlayDependencies, }; } catch (error) { if (error && typeof error === 'object' && 'errors' in error) { const errors = Array.isArray((error as { errors?: Message[] }).errors) ? (error as { errors: Message[] }).errors.map(formatEsbuildMessage) : ['Play bundling failed.']; return { success: false, filePath: absolutePath, errors, }; } return { success: false, filePath: absolutePath, errors: [error instanceof Error ? error.message : String(error)], }; } }