@stream44.studio/encapsulate 0.2.0-rc.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.
@@ -0,0 +1,1591 @@
1
+
2
+ import { join, normalize, dirname, resolve, relative } from 'path'
3
+ import { readFile, stat } from 'fs/promises'
4
+ import * as ts from 'typescript'
5
+ import { createHash } from 'crypto'
6
+
7
+ // Known exports from @stream44.studio/encapsulate/encapsulate that can be imported
8
+ const ENCAPSULATE_MODULE_EXPORTS = new Set([
9
+ 'CapsulePropertyTypes',
10
+ 'makeImportStack',
11
+ 'Spine',
12
+ 'SpineRuntime',
13
+ 'join',
14
+ ])
15
+
16
+ /**
17
+ * Finds the nearest package.json and constructs an npm URI for the given filepath
18
+ * @param absoluteFilepath - The absolute path to the file
19
+ * @param spineRoot - The spine filesystem root
20
+ * @returns The npm URI (e.g., '@scope/package/path/to/file.ts') or null if not found
21
+ */
22
+ async function constructNpmUri(absoluteFilepath: string, spineRoot: string): Promise<string | null> {
23
+ let currentDir = dirname(absoluteFilepath)
24
+ const maxDepth = 20 // Prevent infinite loops
25
+
26
+ for (let i = 0; i < maxDepth; i++) {
27
+ const packageJsonPath = join(currentDir, 'package.json')
28
+
29
+ try {
30
+ await stat(packageJsonPath)
31
+ // Found package.json, read it
32
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
33
+ const packageName = packageJson.name
34
+
35
+ if (!packageName) {
36
+ // No name in package.json, continue searching
37
+ currentDir = dirname(currentDir)
38
+ continue
39
+ }
40
+
41
+ // Get the relative path from the package root to the file
42
+ const relativeFromPackage = relative(currentDir, absoluteFilepath)
43
+
44
+ // Construct npm URI: packageName/relativePath
45
+ return `${packageName}/${relativeFromPackage}`
46
+ } catch (error) {
47
+ // package.json not found or not readable, go up one directory
48
+ const parentDir = dirname(currentDir)
49
+ if (parentDir === currentDir) {
50
+ // Reached filesystem root
51
+ break
52
+ }
53
+ currentDir = parentDir
54
+ }
55
+ }
56
+
57
+ return null
58
+ }
59
+
60
+ // Native JavaScript APIs that don't require explicit ambient reference declaration
61
+ // These are module-global builtins available in all JavaScript runtimes
62
+ const MODULE_GLOBAL_BUILTINS = new Set([
63
+
64
+ 'process',
65
+
66
+ // Console API
67
+ 'console',
68
+
69
+ // Timers
70
+ 'setTimeout',
71
+ 'setInterval',
72
+ 'clearTimeout',
73
+ 'clearInterval',
74
+ 'setImmediate',
75
+ 'clearImmediate',
76
+
77
+ // Encoding/Decoding
78
+ 'atob',
79
+ 'btoa',
80
+ 'TextEncoder',
81
+ 'TextDecoder',
82
+
83
+ // URL APIs
84
+ 'URL',
85
+ 'URLSearchParams',
86
+
87
+ // Fetch API
88
+ 'fetch',
89
+ 'Request',
90
+ 'Response',
91
+ 'Headers',
92
+
93
+ // Crypto
94
+ 'crypto',
95
+ 'Crypto',
96
+ 'SubtleCrypto',
97
+
98
+ // Structured Clone
99
+ 'structuredClone',
100
+
101
+ // Error types
102
+ 'Error',
103
+ 'TypeError',
104
+ 'RangeError',
105
+ 'SyntaxError',
106
+ 'ReferenceError',
107
+ 'EvalError',
108
+ 'URIError',
109
+ 'AggregateError',
110
+
111
+ // Collections
112
+ 'Array',
113
+ 'Map',
114
+ 'Set',
115
+ 'WeakMap',
116
+ 'WeakSet',
117
+ 'Record',
118
+
119
+ // Typed Arrays
120
+ 'ArrayBuffer',
121
+ 'SharedArrayBuffer',
122
+ 'DataView',
123
+ 'Int8Array',
124
+ 'Uint8Array',
125
+ 'Uint8ClampedArray',
126
+ 'Int16Array',
127
+ 'Uint16Array',
128
+ 'Int32Array',
129
+ 'Uint32Array',
130
+ 'Float32Array',
131
+ 'Float64Array',
132
+ 'BigInt64Array',
133
+ 'BigUint64Array',
134
+
135
+ // Other standard builtins
136
+ 'Object',
137
+ 'Function',
138
+ 'Boolean',
139
+ 'Symbol',
140
+ 'Number',
141
+ 'BigInt',
142
+ 'Math',
143
+ 'Date',
144
+ 'String',
145
+ 'RegExp',
146
+ 'JSON',
147
+ 'Promise',
148
+ 'Proxy',
149
+ 'Reflect',
150
+ 'Intl',
151
+ 'WebAssembly',
152
+
153
+ // Global functions
154
+ 'isNaN',
155
+ 'isFinite',
156
+ 'parseInt',
157
+ 'parseFloat',
158
+ 'encodeURI',
159
+ 'encodeURIComponent',
160
+ 'decodeURI',
161
+ 'decodeURIComponent',
162
+ 'escape',
163
+ 'unescape',
164
+ ])
165
+
166
+ export function StaticAnalyzer({
167
+ timing,
168
+ cacheStore,
169
+ spineStore
170
+ }: {
171
+ timing?: { record: (step: string) => void, chalk?: any },
172
+ cacheStore?: {
173
+ writeFile?: (filepath: string, content: string) => Promise<void>,
174
+ readFile?: (filepath: string) => Promise<string | undefined>,
175
+ getStats?: (filepath: string) => Promise<{ mtime: Date } | null>
176
+ },
177
+ spineStore?: {
178
+ getStats?: (filepath: string) => Promise<{ mtime: Date } | null>
179
+ }
180
+ } = {}) {
181
+
182
+ timing?.record('StaticAnalyzer: Initialized')
183
+
184
+ return {
185
+
186
+ parseModule: async ({ spineOptions, encapsulateOptions }: { spineOptions: any, encapsulateOptions: any }) => {
187
+
188
+ const moduleFilepath = join(spineOptions.spineFilesystemRoot, encapsulateOptions.moduleFilepath)
189
+
190
+ // Determine the cache file path based on whether the module is external or internal
191
+ let cacheFilePath: string
192
+ if (encapsulateOptions.moduleFilepath.startsWith('../')) {
193
+ // External module - construct npm URI
194
+ const npmUri = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
195
+ if (npmUri) {
196
+ cacheFilePath = npmUri
197
+ } else {
198
+ // Fallback to normalized path if npm URI construction fails
199
+ cacheFilePath = normalize(encapsulateOptions.moduleFilepath).replace(/^\.\.\//, '').replace(/\.\.\//g, '')
200
+ }
201
+ } else {
202
+ // Internal module - use relative path as-is
203
+ cacheFilePath = encapsulateOptions.moduleFilepath
204
+ }
205
+
206
+ const capsuleSourceLineRef = `${cacheFilePath}:${encapsulateOptions.importStackLine}`
207
+
208
+ // Try to load from cache first
209
+ if (cacheStore?.readFile && cacheStore?.getStats && spineStore?.getStats) {
210
+ try {
211
+ // Check if cache files exist and are newer than source file
212
+ const [cstsStats, crtsStats, sourceStats] = await Promise.all([
213
+ cacheStore.getStats(`${capsuleSourceLineRef}.csts.json`),
214
+ cacheStore.getStats(`${capsuleSourceLineRef}.crts.json`),
215
+ spineStore.getStats(encapsulateOptions.moduleFilepath)
216
+ ])
217
+
218
+ // Use cache if both cache files exist and are newer than source file
219
+ if (cstsStats && crtsStats && sourceStats &&
220
+ cstsStats.mtime >= sourceStats.mtime &&
221
+ crtsStats.mtime >= sourceStats.mtime) {
222
+
223
+ const [cstsContent, crtsContent] = await Promise.all([
224
+ cacheStore.readFile(`${capsuleSourceLineRef}.csts.json`),
225
+ cacheStore.readFile(`${capsuleSourceLineRef}.crts.json`)
226
+ ])
227
+
228
+ if (cstsContent && crtsContent) {
229
+ timing?.record(`StaticAnalyzer: Cache HIT for ${encapsulateOptions.moduleFilepath}`)
230
+ return {
231
+ csts: JSON.parse(cstsContent),
232
+ crts: JSON.parse(crtsContent)
233
+ }
234
+ }
235
+ }
236
+ timing?.record(timing?.chalk?.red?.(`StaticAnalyzer: Cache MISS for ${encapsulateOptions.moduleFilepath}`))
237
+ } catch (error) {
238
+ // Cache miss or error, continue with normal parsing
239
+ timing?.record(timing?.chalk?.red?.(`StaticAnalyzer: Cache error for ${encapsulateOptions.moduleFilepath}`))
240
+ }
241
+ }
242
+
243
+ timing?.record(`StaticAnalyzer: Reading source file ${encapsulateOptions.moduleFilepath}`)
244
+
245
+ const csts: Record<string, any> = {}
246
+ const crts = {}
247
+
248
+ try {
249
+ // Read source file
250
+ timing?.record(`StaticAnalyzer: About to read file ${encapsulateOptions.moduleFilepath}`)
251
+ const readStart = Date.now()
252
+ const sourceCode = await readFile(moduleFilepath, 'utf-8')
253
+ const readDuration = Date.now() - readStart
254
+ timing?.record(`StaticAnalyzer: Read file took ${readDuration}ms for ${encapsulateOptions.moduleFilepath}`)
255
+
256
+ // Parse with TypeScript
257
+ timing?.record(`StaticAnalyzer: About to parse with TypeScript ${encapsulateOptions.moduleFilepath}`)
258
+ const parseStart = Date.now()
259
+ const sourceFile = ts.createSourceFile(
260
+ moduleFilepath,
261
+ sourceCode,
262
+ ts.ScriptTarget.Latest,
263
+ true
264
+ )
265
+ const parseDuration = Date.now() - parseStart
266
+ timing?.record(`StaticAnalyzer: TypeScript parse took ${parseDuration}ms for ${encapsulateOptions.moduleFilepath}`)
267
+
268
+ // Build import map for the file
269
+ const importMap = buildImportMap(sourceFile)
270
+
271
+ // Build assignment map for variables assigned from imported functions
272
+ const assignmentMap = buildAssignmentMap(sourceFile, importMap)
273
+
274
+ // Find all encapsulate() calls
275
+ const encapsulateCalls = findEncapsulateCalls(sourceFile)
276
+
277
+ // Process each encapsulate call
278
+ for (const call of encapsulateCalls) {
279
+ const declarationLine = sourceFile.getLineAndCharacterOfPosition(call.pos).line + 1
280
+
281
+ // Check if this call contains a makeImportStack() call on the importStackLine
282
+ let hasMatchingImportStack = false
283
+ if (call.arguments.length > 1 && ts.isObjectLiteralExpression(call.arguments[1])) {
284
+ const optionsObject = call.arguments[1]
285
+ for (const prop of optionsObject.properties) {
286
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'importStack') {
287
+ if (ts.isCallExpression(prop.initializer) && ts.isIdentifier(prop.initializer.expression) && prop.initializer.expression.text === 'makeImportStack') {
288
+ const importStackCallLine = sourceFile.getLineAndCharacterOfPosition(prop.initializer.pos).line + 1
289
+ if (importStackCallLine === encapsulateOptions.importStackLine) {
290
+ hasMatchingImportStack = true
291
+ break
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ // Only process calls that match the importStackLine
299
+ if (!hasMatchingImportStack) {
300
+ continue
301
+ }
302
+
303
+ const capsuleSourceLineRef = `${encapsulateOptions.moduleFilepath}:${encapsulateOptions.importStackLine}`
304
+ const capsuleSourceNameRef = encapsulateOptions.capsuleName && `${encapsulateOptions.moduleFilepath}:${encapsulateOptions.capsuleName}`
305
+ const capsuleSourceNameRefHash = capsuleSourceNameRef && createHash('md5').update(capsuleSourceNameRef).digest('hex')
306
+
307
+ // Construct npm URI for the module - try for all modules
308
+ let moduleUri: string | null = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
309
+
310
+ // If npm URI construction failed, fall back to moduleFilepath
311
+ if (!moduleUri) {
312
+ moduleUri = encapsulateOptions.moduleFilepath
313
+ }
314
+
315
+ // Strip file extension from URI
316
+ const moduleUriWithoutExt = moduleUri.replace(/\.(ts|tsx|js|jsx)$/, '')
317
+ const capsuleSourceUriLineRef = `${moduleUriWithoutExt}:${encapsulateOptions.importStackLine}`
318
+
319
+ // Store moduleUri without extension
320
+ moduleUri = moduleUriWithoutExt
321
+
322
+ // Extract the capsule expression text from the source
323
+ const capsuleExpression = call.getText(sourceFile)
324
+
325
+ const ambientReferences = extractCapsuleAmbientReferences(
326
+ call,
327
+ sourceFile,
328
+ encapsulateOptions.ambientReferences,
329
+ importMap,
330
+ assignmentMap
331
+ )
332
+
333
+ const cst: any = {
334
+ capsuleSourceLineRef,
335
+ capsuleSourceNameRef,
336
+ capsuleSourceNameRefHash,
337
+ capsuleSourceUriLineRef,
338
+ source: {
339
+ moduleFilepath: encapsulateOptions.moduleFilepath,
340
+ moduleUri,
341
+ capsuleName: encapsulateOptions.capsuleName,
342
+ declarationLine,
343
+ importStackLine: encapsulateOptions.importStackLine,
344
+ capsuleExpression,
345
+ ambientReferences,
346
+ moduleLocalCode: extractModuleLocalCode(
347
+ ambientReferences,
348
+ sourceFile,
349
+ importMap,
350
+ assignmentMap,
351
+ call
352
+ )
353
+ },
354
+ spineContracts: {}
355
+ }
356
+
357
+ // Parse the first argument (capsule definition object)
358
+ if (call.arguments.length > 0 && ts.isObjectLiteralExpression(call.arguments[0])) {
359
+ const definitionObject = call.arguments[0]
360
+
361
+ // Get start and end line numbers for the definition object
362
+ const defStartPos = sourceFile.getLineAndCharacterOfPosition(definitionObject.getStart(sourceFile))
363
+ const defEndPos = sourceFile.getLineAndCharacterOfPosition(definitionObject.getEnd())
364
+ cst.source.definitionStartLine = defStartPos.line + 1
365
+ cst.source.definitionEndLine = defEndPos.line + 1
366
+
367
+ // Parse the second argument (options object) if present
368
+ if (call.arguments.length > 1 && ts.isObjectLiteralExpression(call.arguments[1])) {
369
+ const optionsObject = call.arguments[1]
370
+ const optionsStartPos = sourceFile.getLineAndCharacterOfPosition(optionsObject.getStart(sourceFile))
371
+ const optionsEndPos = sourceFile.getLineAndCharacterOfPosition(optionsObject.getEnd())
372
+ cst.source.optionsStartLine = optionsStartPos.line + 1
373
+ cst.source.optionsEndLine = optionsEndPos.line + 1
374
+ }
375
+
376
+ // Parse spineContract definitions (e.g., '$spineContract1': { ... })
377
+ for (const spineContractProp of definitionObject.properties) {
378
+ let spineContractName: string | null = null
379
+
380
+ // Handle string literal property names
381
+ if (ts.isPropertyAssignment(spineContractProp) && ts.isStringLiteral(spineContractProp.name)) {
382
+ spineContractName = spineContractProp.name.text
383
+ }
384
+ // Handle computed property names like ['#' + CapsuleSpineContract['#']]
385
+ else if (ts.isPropertyAssignment(spineContractProp) && ts.isComputedPropertyName(spineContractProp.name)) {
386
+ const computedName = spineContractProp.name.expression
387
+ // Try to resolve the computed name from ambient references
388
+ const computedText = computedName.getText(sourceFile)
389
+ // Check if it matches pattern like "'#' + SomeVar['#']" or '"#" + SomeVar["#"]'
390
+ const match = computedText.match(/['"]#['"] \+ (\w+)\[['"]#['"]\]/)
391
+ if (match && ambientReferences[match[1]]) {
392
+ const refValue = ambientReferences[match[1]].value
393
+ if (refValue && refValue['#']) {
394
+ spineContractName = '#' + refValue['#']
395
+ }
396
+ }
397
+ }
398
+
399
+ if (spineContractName && ts.isPropertyAssignment(spineContractProp)) {
400
+ const spineContractValue = spineContractProp.initializer
401
+
402
+ if (ts.isObjectLiteralExpression(spineContractValue)) {
403
+ const spineContractDef: any = {
404
+ properties: {}
405
+ }
406
+
407
+ // Parse properties within the spineContract
408
+ for (const prop of spineContractValue.properties) {
409
+ if (ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {
410
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : (prop.name as ts.StringLiteral).text
411
+ const propValue = prop.initializer
412
+
413
+ // Check if this is a property contract key (starts with '#')
414
+ if (propName.startsWith('#')) {
415
+ const propertyContractUri = propName.substring(1) // Remove the '#' prefix
416
+
417
+ if (ts.isObjectLiteralExpression(propValue)) {
418
+ // Create property contract entry
419
+ if (!spineContractDef.properties[propName]) {
420
+ spineContractDef.properties[propName] = {
421
+ propertyContractUri,
422
+ properties: {}
423
+ }
424
+ }
425
+
426
+ // Parse properties within the property contract
427
+ for (const contractProp of propValue.properties) {
428
+ if (ts.isPropertyAssignment(contractProp) && (ts.isIdentifier(contractProp.name) || ts.isStringLiteral(contractProp.name))) {
429
+ const contractPropName = ts.isIdentifier(contractProp.name) ? contractProp.name.text : (contractProp.name as ts.StringLiteral).text
430
+ const contractPropValue = contractProp.initializer
431
+
432
+ if (ts.isObjectLiteralExpression(contractPropValue)) {
433
+ const propDef: any = {}
434
+
435
+ // Get line numbers for the property
436
+ const propDeclarationPos = sourceFile.getLineAndCharacterOfPosition(contractProp.name.getStart(sourceFile))
437
+ propDef.declarationLine = propDeclarationPos.line + 1
438
+
439
+ const propDefStartPos = sourceFile.getLineAndCharacterOfPosition(contractPropValue.getStart(sourceFile))
440
+ const propDefEndPos = sourceFile.getLineAndCharacterOfPosition(contractPropValue.getEnd())
441
+ propDef.definitionStartLine = propDefStartPos.line + 1
442
+ propDef.definitionEndLine = propDefEndPos.line + 1
443
+
444
+ // Extract property definition fields
445
+ for (const field of contractPropValue.properties) {
446
+ if (ts.isPropertyAssignment(field) && ts.isIdentifier(field.name)) {
447
+ const fieldName = field.name.text
448
+ const fieldValue = field.initializer
449
+
450
+ if (fieldName === 'type') {
451
+ // Extract type value
452
+ if (ts.isPropertyAccessExpression(fieldValue)) {
453
+ propDef.type = fieldValue.getText(sourceFile)
454
+ }
455
+ } else if (fieldName === 'value') {
456
+ // Capture the TS type of the value
457
+ const valueType = extractValueType(fieldValue, sourceFile)
458
+ propDef.valueType = valueType
459
+
460
+ // Store the value expression as text
461
+ propDef.valueExpression = fieldValue.getText(sourceFile)
462
+
463
+ // Extract ambient references if it's a function
464
+ if (ts.isFunctionExpression(fieldValue) || ts.isArrowFunction(fieldValue)) {
465
+ propDef.ambientReferences = extractAndValidateAmbientReferences(
466
+ fieldValue,
467
+ sourceFile,
468
+ encapsulateOptions.ambientReferences,
469
+ contractPropName,
470
+ spineContractName,
471
+ importMap,
472
+ assignmentMap
473
+ )
474
+
475
+ // Extract module-local code for property functions
476
+ propDef.moduleLocalCode = extractModuleLocalCode(
477
+ propDef.ambientReferences,
478
+ sourceFile,
479
+ importMap,
480
+ assignmentMap
481
+ )
482
+ }
483
+ } else if (fieldName === 'kind') {
484
+ propDef.kind = fieldValue.getText(sourceFile)
485
+ } else if (fieldName === 'projections') {
486
+ propDef.projections = fieldValue.getText(sourceFile)
487
+ } else if (fieldName === 'tags') {
488
+ propDef.tags = fieldValue.getText(sourceFile)
489
+ }
490
+ }
491
+ }
492
+
493
+ spineContractDef.properties[propName].properties[contractPropName] = propDef
494
+ }
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+ cst.spineContracts[spineContractName] = spineContractDef
503
+ }
504
+ }
505
+ }
506
+ }
507
+
508
+ // Add dynamic property contract mappings to the CST
509
+ // For each non-default property contract, create a mapping in the '#' contract
510
+ for (const [spineContractName, spineContractDef] of Object.entries(cst.spineContracts)) {
511
+ const spineContract = spineContractDef as any
512
+ if (spineContract.properties) {
513
+ // Find all non-default property contracts
514
+ const nonDefaultContracts: string[] = []
515
+ for (const propName of Object.keys(spineContract.properties)) {
516
+ if (propName.startsWith('#') && propName !== '#') {
517
+ nonDefaultContracts.push(propName)
518
+ }
519
+ }
520
+
521
+ // Add dynamic mappings to the '#' contract
522
+ if (nonDefaultContracts.length > 0) {
523
+ // Ensure '#' contract exists
524
+ if (!spineContract.properties['#']) {
525
+ spineContract.properties['#'] = {
526
+ propertyContractUri: '',
527
+ properties: {}
528
+ }
529
+ }
530
+ if (!spineContract.properties['#'].properties) {
531
+ spineContract.properties['#'].properties = {}
532
+ }
533
+
534
+ // Add a dynamic mapping for each non-default property contract
535
+ for (const propContractUri of nonDefaultContracts) {
536
+ const contractKey = '#' + propContractUri.substring(1)
537
+ spineContract.properties['#'].properties[contractKey] = {
538
+ declarationLine: -1,
539
+ definitionStartLine: -1,
540
+ definitionEndLine: -1,
541
+ type: 'CapsulePropertyTypes.Mapping',
542
+ valueType: 'string',
543
+ valueExpression: `"${propContractUri.substring(1)}"`,
544
+ propertyContractDelegate: propContractUri
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+
551
+ csts[capsuleSourceLineRef] = cst
552
+ }
553
+
554
+ } catch (error) {
555
+ console.error(`Error parsing module '${moduleFilepath}':`, error)
556
+ throw error
557
+ }
558
+
559
+ timing?.record(`StaticAnalyzer: Parsing complete for ${encapsulateOptions.moduleFilepath}`)
560
+
561
+ // Save to cache
562
+ if (cacheStore?.writeFile) {
563
+ try {
564
+ timing?.record(`StaticAnalyzer: Writing cache for ${encapsulateOptions.moduleFilepath}`)
565
+ await Promise.all([
566
+ cacheStore.writeFile(`${capsuleSourceLineRef}.csts.json`, JSON.stringify(csts, null, 2)),
567
+ cacheStore.writeFile(`${capsuleSourceLineRef}.crts.json`, JSON.stringify(crts, null, 2))
568
+ ])
569
+ } catch (error) {
570
+ // Cache write error, continue without failing
571
+ console.warn(`Warning: Failed to write to cache for ${capsuleSourceLineRef}:`, error)
572
+ }
573
+ }
574
+
575
+ return {
576
+ csts,
577
+ crts,
578
+ }
579
+ }
580
+ }
581
+ }
582
+
583
+ // Build a map of imported identifiers
584
+ function buildImportMap(sourceFile: ts.SourceFile): Map<string, { importSpecifier: string, moduleUri: string }> {
585
+ const importMap = new Map<string, { importSpecifier: string, moduleUri: string }>()
586
+
587
+ for (const statement of sourceFile.statements) {
588
+ if (ts.isImportDeclaration(statement)) {
589
+ const moduleSpecifier = (statement.moduleSpecifier as ts.StringLiteral).text
590
+ const importClause = statement.importClause
591
+
592
+ if (importClause) {
593
+ // Handle named imports: import { foo, bar } from 'module'
594
+ if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
595
+ const elements = importClause.namedBindings.elements
596
+
597
+ for (const element of elements) {
598
+ const importedName = element.name.text
599
+ const originalName = element.propertyName ? element.propertyName.text : importedName
600
+
601
+ const importSpecifier = element.propertyName
602
+ ? `{ ${originalName} as ${importedName} }`
603
+ : `{ ${importedName} }`
604
+
605
+ importMap.set(importedName, {
606
+ importSpecifier,
607
+ moduleUri: moduleSpecifier
608
+ })
609
+ }
610
+ }
611
+
612
+ // Handle default imports: import foo from 'module'
613
+ if (importClause.name) {
614
+ const defaultName = importClause.name.text
615
+ importMap.set(defaultName, {
616
+ importSpecifier: defaultName,
617
+ moduleUri: moduleSpecifier
618
+ })
619
+ }
620
+
621
+ // Handle namespace imports: import * as foo from 'module'
622
+ if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) {
623
+ const namespaceName = importClause.namedBindings.name.text
624
+ importMap.set(namespaceName, {
625
+ importSpecifier: `* as ${namespaceName}`,
626
+ moduleUri: moduleSpecifier
627
+ })
628
+ }
629
+ }
630
+ }
631
+ }
632
+
633
+ return importMap
634
+ }
635
+
636
+ // Build a map of variables assigned from imported function calls
637
+ function buildAssignmentMap(
638
+ sourceFile: ts.SourceFile,
639
+ importMap: Map<string, { importSpecifier: string, moduleUri: string }>
640
+ ): Map<string, { importSpecifier: string, moduleUri: string }> {
641
+ const assignmentMap = new Map<string, { importSpecifier: string, moduleUri: string }>()
642
+
643
+ function visit(node: ts.Node) {
644
+ // Look for variable declarations with destructuring from function calls
645
+ // e.g., const { foo, bar } = importedFunction() or const { foo, bar } = await importedFunction()
646
+ if (ts.isVariableDeclaration(node)) {
647
+ let callExpr: ts.CallExpression | undefined
648
+
649
+ // Check if it has an initializer that is a call expression or await expression
650
+ if (node.initializer) {
651
+ if (ts.isCallExpression(node.initializer)) {
652
+ callExpr = node.initializer
653
+ } else if (ts.isAwaitExpression(node.initializer) && ts.isCallExpression(node.initializer.expression)) {
654
+ callExpr = node.initializer.expression
655
+ }
656
+ }
657
+
658
+ if (callExpr) {
659
+ // Check if the function being called is an imported identifier
660
+ if (ts.isIdentifier(callExpr.expression)) {
661
+ const functionName = callExpr.expression.text
662
+ const importInfo = importMap.get(functionName)
663
+
664
+ if (importInfo) {
665
+ // This is a call to an imported function
666
+ // Track all destructured variables
667
+ if (ts.isObjectBindingPattern(node.name)) {
668
+ for (const element of node.name.elements) {
669
+ if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
670
+ const varName = element.name.text
671
+ assignmentMap.set(varName, {
672
+ importSpecifier: importInfo.importSpecifier,
673
+ moduleUri: importInfo.moduleUri
674
+ })
675
+ }
676
+ }
677
+ }
678
+ }
679
+ }
680
+ }
681
+ }
682
+
683
+ ts.forEachChild(node, visit)
684
+ }
685
+
686
+ visit(sourceFile)
687
+ return assignmentMap
688
+ }
689
+
690
+ // Find all encapsulate() call expressions in the source file
691
+ function findEncapsulateCalls(sourceFile: ts.SourceFile): ts.CallExpression[] {
692
+ const calls: ts.CallExpression[] = []
693
+
694
+ function visit(node: ts.Node) {
695
+ if (ts.isCallExpression(node)) {
696
+ // Check if it's a call to encapsulate
697
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'encapsulate') {
698
+ calls.push(node)
699
+ }
700
+ }
701
+ ts.forEachChild(node, visit)
702
+ }
703
+
704
+ visit(sourceFile)
705
+ return calls
706
+ }
707
+
708
+ // Extract the TypeScript type of a value expression
709
+ function extractValueType(valueNode: ts.Expression, sourceFile: ts.SourceFile): string {
710
+ // Check for function expressions
711
+ if (ts.isFunctionExpression(valueNode) || ts.isArrowFunction(valueNode)) {
712
+ return extractFunctionSignature(valueNode, sourceFile)
713
+ }
714
+
715
+ // Check for undefined
716
+ if (valueNode.kind === ts.SyntaxKind.UndefinedKeyword) {
717
+ return 'undefined'
718
+ }
719
+
720
+ // Check for null
721
+ if (valueNode.kind === ts.SyntaxKind.NullKeyword) {
722
+ return 'null'
723
+ }
724
+
725
+ // Check for string literals
726
+ if (ts.isStringLiteral(valueNode)) {
727
+ return 'string'
728
+ }
729
+
730
+ // Check for numeric literals
731
+ if (ts.isNumericLiteral(valueNode)) {
732
+ return 'number'
733
+ }
734
+
735
+ // Check for boolean literals
736
+ if (valueNode.kind === ts.SyntaxKind.TrueKeyword || valueNode.kind === ts.SyntaxKind.FalseKeyword) {
737
+ return 'boolean'
738
+ }
739
+
740
+ // Check for object literals
741
+ if (ts.isObjectLiteralExpression(valueNode)) {
742
+ return 'object'
743
+ }
744
+
745
+ // Check for array literals
746
+ if (ts.isArrayLiteralExpression(valueNode)) {
747
+ return 'array'
748
+ }
749
+
750
+ // Default to any
751
+ return 'any'
752
+ }
753
+
754
+ // Extract function signature from AST
755
+ function extractFunctionSignature(fn: ts.FunctionExpression | ts.ArrowFunction, sourceFile: ts.SourceFile): string {
756
+ const params: string[] = []
757
+
758
+ for (const param of fn.parameters) {
759
+ let paramStr = param.dotDotDotToken ? '...' : ''
760
+ paramStr += param.name.getText(sourceFile)
761
+ if (param.type) {
762
+ paramStr += `: ${param.type.getText(sourceFile)}`
763
+ } else {
764
+ paramStr += ': any'
765
+ }
766
+ if (param.questionToken) {
767
+ paramStr = paramStr.replace(':', '?:')
768
+ }
769
+ params.push(paramStr)
770
+ }
771
+
772
+ let returnType = 'any'
773
+ if (fn.type) {
774
+ returnType = fn.type.getText(sourceFile)
775
+ }
776
+
777
+ return `(${params.join(', ')}) => ${returnType}`
778
+ }
779
+
780
+ // Extract module-local functions that are self-contained
781
+ function extractModuleLocalCode(
782
+ ambientReferences: Record<string, any>,
783
+ sourceFile: ts.SourceFile,
784
+ importMap: Map<string, { importSpecifier: string, moduleUri: string }>,
785
+ assignmentMap: Map<string, { importSpecifier: string, moduleUri: string }>,
786
+ callNode?: ts.Node
787
+ ): Record<string, string> {
788
+ const moduleLocalCode: Record<string, string> = {}
789
+ const moduleLocalFunctions = new Map<string, ts.FunctionDeclaration>()
790
+
791
+ // First, collect all top-level function declarations in the module (including async functions)
792
+ for (const statement of sourceFile.statements) {
793
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
794
+ moduleLocalFunctions.set(statement.name.text, statement)
795
+ }
796
+ }
797
+
798
+ // Also collect functions from the local scope around the call node
799
+ if (callNode) {
800
+ let currentNode: ts.Node | undefined = callNode
801
+ while (currentNode) {
802
+ if (ts.isFunctionExpression(currentNode) || ts.isArrowFunction(currentNode) || ts.isFunctionDeclaration(currentNode)) {
803
+ if (currentNode.body && ts.isBlock(currentNode.body)) {
804
+ for (const statement of currentNode.body.statements) {
805
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
806
+ moduleLocalFunctions.set(statement.name.text, statement)
807
+ }
808
+ }
809
+ }
810
+ break
811
+ }
812
+ currentNode = currentNode.parent
813
+ }
814
+ }
815
+
816
+ // Check each ambient reference to see if it's a module-local function
817
+ for (const [name, ref] of Object.entries(ambientReferences)) {
818
+ const refTyped = ref as any
819
+
820
+ // Skip if it's not a literal ambient reference or module-local (imports, assignments, etc. are handled elsewhere)
821
+ if (refTyped.type !== 'literal' && refTyped.type !== 'object' && refTyped.type !== 'capsule' && refTyped.type !== 'module-local') {
822
+ continue
823
+ }
824
+
825
+ // Check if this identifier refers to a module-local function
826
+ const funcDecl = moduleLocalFunctions.get(name)
827
+ if (!funcDecl) continue
828
+
829
+ // Analyze the function to see if it's self-contained
830
+ const dependencies = new Set<string>()
831
+ const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
832
+
833
+ if (isContained) {
834
+ // Mark this as module-local in ambient references
835
+ refTyped.type = 'module-local'
836
+
837
+ // Collect the function code and all its dependencies
838
+ const collectedCode: string[] = []
839
+ const processed = new Set<string>()
840
+
841
+ function collectFunction(fnName: string) {
842
+ if (processed.has(fnName)) return
843
+ processed.add(fnName)
844
+
845
+ const fn = moduleLocalFunctions.get(fnName)
846
+ if (fn) {
847
+ const fnCode = fn.getText(sourceFile)
848
+ collectedCode.push(fnCode)
849
+ // Also add each function as a separate entry in moduleLocalCode
850
+ if (!moduleLocalCode[fnName]) {
851
+ moduleLocalCode[fnName] = fnCode
852
+ }
853
+ }
854
+ }
855
+
856
+ // Collect the main function
857
+ collectFunction(name)
858
+
859
+ // Collect all dependencies
860
+ for (const dep of dependencies) {
861
+ collectFunction(dep)
862
+ }
863
+
864
+ // Store the collected code (main function with all dependencies)
865
+ moduleLocalCode[name] = collectedCode.join('\n\n')
866
+ }
867
+ }
868
+
869
+ // Also process any module-local functions that weren't in ambient references
870
+ // but might be dependencies of other functions
871
+ for (const [fnName, funcDecl] of moduleLocalFunctions.entries()) {
872
+ // Skip if already processed
873
+ if (moduleLocalCode[fnName]) continue
874
+
875
+ // Analyze if it's self-contained
876
+ const dependencies = new Set<string>()
877
+ const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
878
+
879
+ if (isContained) {
880
+ // Add this function to moduleLocalCode
881
+ moduleLocalCode[fnName] = funcDecl.getText(sourceFile)
882
+ }
883
+ }
884
+
885
+ return moduleLocalCode
886
+ }
887
+
888
+ // Analyze if a function is self-contained (only depends on other module-local functions or builtins)
889
+ function analyzeFunctionDependencies(
890
+ funcDecl: ts.FunctionDeclaration,
891
+ sourceFile: ts.SourceFile,
892
+ importMap: Map<string, { importSpecifier: string, moduleUri: string }>,
893
+ assignmentMap: Map<string, { importSpecifier: string, moduleUri: string }>,
894
+ moduleLocalFunctions: Map<string, ts.FunctionDeclaration>,
895
+ dependencies: Set<string>
896
+ ): boolean {
897
+ const localIdentifiers = new Set<string>()
898
+ const nestedFunctionScopes = new Map<ts.Node, Set<string>>()
899
+
900
+ // Collect parameter names from the main function
901
+ if (funcDecl.parameters) {
902
+ for (const param of funcDecl.parameters) {
903
+ extractBindingIdentifiersForAnalysis(param.name, localIdentifiers)
904
+ }
905
+ }
906
+
907
+ let isContained = true
908
+
909
+ // First pass: collect all nested function declarations and their scopes
910
+ function collectNestedFunctions(node: ts.Node, currentScope: Set<string>) {
911
+ // Track variable declarations in current scope
912
+ if (ts.isVariableDeclaration(node)) {
913
+ extractBindingIdentifiersForAnalysis(node.name, currentScope)
914
+ }
915
+
916
+ // Track nested function declarations
917
+ if (ts.isFunctionDeclaration(node) && node.name) {
918
+ currentScope.add(node.name.text)
919
+ // Create a new scope for this nested function
920
+ const nestedScope = new Set<string>(currentScope)
921
+ nestedFunctionScopes.set(node, nestedScope)
922
+ // Add parameters to nested scope
923
+ for (const param of node.parameters) {
924
+ extractBindingIdentifiersForAnalysis(param.name, nestedScope)
925
+ }
926
+ // Continue traversing within the nested function
927
+ if (node.body) {
928
+ ts.forEachChild(node.body, (child) => collectNestedFunctions(child, nestedScope))
929
+ }
930
+ return // Don't traverse children again
931
+ }
932
+
933
+ // Track nested function expressions and arrow functions
934
+ if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
935
+ const nestedScope = new Set<string>(currentScope)
936
+ nestedFunctionScopes.set(node, nestedScope)
937
+ // Add parameters to nested scope
938
+ for (const param of node.parameters) {
939
+ extractBindingIdentifiersForAnalysis(param.name, nestedScope)
940
+ }
941
+ // Add function name if it's a named function expression
942
+ if (ts.isFunctionExpression(node) && node.name) {
943
+ nestedScope.add(node.name.text)
944
+ }
945
+ // Continue traversing within the nested function
946
+ if (node.body) {
947
+ if (ts.isBlock(node.body)) {
948
+ ts.forEachChild(node.body, (child) => collectNestedFunctions(child, nestedScope))
949
+ }
950
+ }
951
+ return // Don't traverse children again
952
+ }
953
+
954
+ ts.forEachChild(node, (child) => collectNestedFunctions(child, currentScope))
955
+ }
956
+
957
+ // Collect nested functions and their scopes
958
+ if (funcDecl.body) {
959
+ collectNestedFunctions(funcDecl.body, localIdentifiers)
960
+ }
961
+
962
+ // Second pass: check for external dependencies
963
+ function visit(node: ts.Node, currentScope: Set<string> = localIdentifiers) {
964
+ // Use the appropriate scope for nested functions
965
+ if (nestedFunctionScopes.has(node)) {
966
+ currentScope = nestedFunctionScopes.get(node)!
967
+ }
968
+
969
+ // Check identifiers
970
+ if (ts.isIdentifier(node)) {
971
+ const identifierName = node.text
972
+
973
+ // Skip special keywords and local identifiers
974
+ if (identifierName === 'this' || identifierName === 'undefined' || identifierName === 'null' ||
975
+ identifierName === 'arguments' || currentScope.has(identifierName)) {
976
+ return
977
+ }
978
+
979
+ // Skip property access names
980
+ const parent = node.parent
981
+ if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) {
982
+ return
983
+ }
984
+
985
+ // Skip property names in object literals
986
+ if (parent && ts.isPropertyAssignment(parent) && parent.name === node) {
987
+ return
988
+ }
989
+
990
+ // Skip shorthand property assignments where the identifier is both key and value
991
+ if (parent && ts.isShorthandPropertyAssignment(parent)) {
992
+ return
993
+ }
994
+
995
+ // Check if it's an import or assignment - track as dependency
996
+ if (importMap.has(identifierName)) {
997
+ dependencies.add(identifierName)
998
+ return
999
+ }
1000
+
1001
+ if (assignmentMap.has(identifierName)) {
1002
+ dependencies.add(identifierName)
1003
+ return
1004
+ }
1005
+
1006
+ // Check if it's a module-local function - add as dependency
1007
+ if (moduleLocalFunctions.has(identifierName)) {
1008
+ dependencies.add(identifierName)
1009
+ return
1010
+ }
1011
+
1012
+ // Check if it's a module-global builtin - allowed
1013
+ if (MODULE_GLOBAL_BUILTINS.has(identifierName)) {
1014
+ return
1015
+ }
1016
+
1017
+ // Unknown external reference - not self-contained
1018
+ isContained = false
1019
+ }
1020
+
1021
+ ts.forEachChild(node, (child) => visit(child, currentScope))
1022
+ }
1023
+
1024
+ if (funcDecl.body) {
1025
+ visit(funcDecl.body)
1026
+ }
1027
+
1028
+ return isContained
1029
+ }
1030
+
1031
+ // Helper to extract binding identifiers for analysis
1032
+ function extractBindingIdentifiersForAnalysis(name: ts.BindingName, targetSet: Set<string>) {
1033
+ if (ts.isIdentifier(name)) {
1034
+ targetSet.add(name.text)
1035
+ } else if (ts.isObjectBindingPattern(name)) {
1036
+ for (const element of name.elements) {
1037
+ if (ts.isBindingElement(element)) {
1038
+ extractBindingIdentifiersForAnalysis(element.name, targetSet)
1039
+ }
1040
+ }
1041
+ } else if (ts.isArrayBindingPattern(name)) {
1042
+ for (const element of name.elements) {
1043
+ if (ts.isBindingElement(element)) {
1044
+ extractBindingIdentifiersForAnalysis(element.name, targetSet)
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ // Extract ambient references from the entire encapsulate call
1051
+ function extractCapsuleAmbientReferences(
1052
+ call: ts.CallExpression,
1053
+ sourceFile: ts.SourceFile,
1054
+ runtimeAmbientRefs: Record<string, any> | undefined,
1055
+ importMap: Map<string, { importSpecifier: string, moduleUri: string }>,
1056
+ assignmentMap: Map<string, { importSpecifier: string, moduleUri: string }>
1057
+ ): Record<string, any> {
1058
+ const ambientRefs: Record<string, any> = {}
1059
+ const localIdentifiers = new Set<string>()
1060
+ const propertyNames = new Set<string>()
1061
+ const invocationParameters = new Set<string>()
1062
+ const moduleLocalFunctions = new Map<string, ts.FunctionDeclaration>()
1063
+
1064
+ // Collect module-local functions from both module top-level and local scope
1065
+ // First, collect top-level module functions
1066
+ for (const statement of sourceFile.statements) {
1067
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
1068
+ moduleLocalFunctions.set(statement.name.text, statement)
1069
+ }
1070
+ }
1071
+
1072
+ // Find enclosing function and collect its parameters and local functions
1073
+ let currentNode: ts.Node | undefined = call
1074
+ let enclosingBlock: ts.Block | undefined
1075
+ while (currentNode) {
1076
+ if (ts.isFunctionExpression(currentNode) || ts.isArrowFunction(currentNode) || ts.isFunctionDeclaration(currentNode)) {
1077
+ // Extract parameter names from this function
1078
+ for (const param of currentNode.parameters) {
1079
+ extractParameterNames(param.name, invocationParameters)
1080
+ }
1081
+ // Get the function body to collect local functions
1082
+ if (currentNode.body && ts.isBlock(currentNode.body)) {
1083
+ enclosingBlock = currentNode.body
1084
+ }
1085
+ break
1086
+ }
1087
+ currentNode = currentNode.parent
1088
+ }
1089
+
1090
+ // Collect function declarations from the enclosing block
1091
+ if (enclosingBlock) {
1092
+ for (const statement of enclosingBlock.statements) {
1093
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
1094
+ moduleLocalFunctions.set(statement.name.text, statement)
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ // Helper to extract parameter names from binding patterns
1100
+ function extractParameterNames(name: ts.BindingName, targetSet: Set<string>) {
1101
+ if (ts.isIdentifier(name)) {
1102
+ targetSet.add(name.text)
1103
+ } else if (ts.isObjectBindingPattern(name)) {
1104
+ for (const element of name.elements) {
1105
+ if (ts.isBindingElement(element)) {
1106
+ extractParameterNames(element.name, targetSet)
1107
+ }
1108
+ }
1109
+ } else if (ts.isArrayBindingPattern(name)) {
1110
+ for (const element of name.elements) {
1111
+ if (ts.isBindingElement(element)) {
1112
+ extractParameterNames(element.name, targetSet)
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ // First pass: collect property names and local identifiers
1119
+ function collectNames(node: ts.Node) {
1120
+ // Track property names in object literals
1121
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
1122
+ propertyNames.add(node.name.text)
1123
+ }
1124
+
1125
+ // Track variable declarations within the call
1126
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
1127
+ localIdentifiers.add(node.name.text)
1128
+ }
1129
+
1130
+ ts.forEachChild(node, collectNames)
1131
+ }
1132
+
1133
+ collectNames(call)
1134
+
1135
+ // Extract CSS imports from comments in the entire call
1136
+ // Pattern: /* import "./file.css"; */ or /* import './file.css' */ (with or without semicolon)
1137
+ const callText = call.getText(sourceFile)
1138
+ const cssImportPattern = /\/\*\s*import\s+["']([^"']+\.css)["']\s*;?\s*\*\//g
1139
+ let match
1140
+
1141
+ while ((match = cssImportPattern.exec(callText)) !== null) {
1142
+ const cssPath = match[1]
1143
+
1144
+ // Add CSS import to ambient references if not already present
1145
+ if (!ambientRefs[cssPath]) {
1146
+ ambientRefs[cssPath] = {
1147
+ type: 'import',
1148
+ importSpecifier: `'${cssPath}'`,
1149
+ moduleUri: cssPath
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ // Second pass: find identifiers used as values (not property names)
1155
+ function visit(node: ts.Node) {
1156
+ // Check for identifiers that might be ambient references
1157
+ if (ts.isIdentifier(node)) {
1158
+ const identifierName = node.text
1159
+
1160
+ // Skip 'this', 'encapsulate', and other special keywords
1161
+ if (identifierName === 'this' || identifierName === 'undefined' || identifierName === 'null' || identifierName === 'encapsulate' || identifierName === 'import') {
1162
+ return
1163
+ }
1164
+
1165
+ // Skip if it's part of import.meta (MetaProperty node)
1166
+ const parent = node.parent
1167
+ if (parent && parent.kind === ts.SyntaxKind.MetaProperty) {
1168
+ return
1169
+ }
1170
+
1171
+ // Skip if it's a property name
1172
+ if (propertyNames.has(identifierName)) {
1173
+ return
1174
+ }
1175
+
1176
+ // Skip if it's a local identifier (local variable)
1177
+ if (localIdentifiers.has(identifierName)) {
1178
+ return
1179
+ }
1180
+
1181
+ // Skip if it's a property access (e.g., this.username)
1182
+ if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) {
1183
+ return
1184
+ }
1185
+
1186
+ // Skip if it's a property name in an object literal
1187
+ if (parent && ts.isPropertyAssignment(parent) && parent.name === node) {
1188
+ return
1189
+ }
1190
+
1191
+ // Check if we already added this reference
1192
+ if (ambientRefs[identifierName]) {
1193
+ return
1194
+ }
1195
+
1196
+ // Check if this is an imported identifier
1197
+ const importInfo = importMap.get(identifierName)
1198
+ if (importInfo) {
1199
+ // This is an import
1200
+ ambientRefs[identifierName] = {
1201
+ type: 'import',
1202
+ importSpecifier: importInfo.importSpecifier,
1203
+ moduleUri: importInfo.moduleUri
1204
+ }
1205
+ } else {
1206
+ // Check if this is an assigned variable from an imported function call
1207
+ const assignmentInfo = assignmentMap.get(identifierName)
1208
+ if (assignmentInfo) {
1209
+ // This is assigned from an imported function
1210
+ ambientRefs[identifierName] = {
1211
+ type: 'assigned',
1212
+ importSpecifier: assignmentInfo.importSpecifier,
1213
+ moduleUri: assignmentInfo.moduleUri
1214
+ }
1215
+ } else if (invocationParameters.has(identifierName)) {
1216
+ // This is an invocation argument (parameter from enclosing function)
1217
+ ambientRefs[identifierName] = {
1218
+ type: 'invocation-argument',
1219
+ isEncapsulateExport: ENCAPSULATE_MODULE_EXPORTS.has(identifierName)
1220
+ }
1221
+ } else {
1222
+ // Check if it's a module-local function
1223
+ const funcDecl = moduleLocalFunctions.get(identifierName)
1224
+ if (funcDecl) {
1225
+ // Analyze if it's self-contained
1226
+ const dependencies = new Set<string>()
1227
+ const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
1228
+
1229
+ if (isContained) {
1230
+ // Mark as module-local
1231
+ ambientRefs[identifierName] = {
1232
+ type: 'module-local'
1233
+ }
1234
+ return
1235
+ }
1236
+ }
1237
+
1238
+ // This is a literal ambient reference
1239
+ // Check if the ambient reference is provided
1240
+ if (runtimeAmbientRefs && identifierName in runtimeAmbientRefs) {
1241
+ const value = runtimeAmbientRefs[identifierName]
1242
+
1243
+ if (isLiteralType(value)) {
1244
+ ambientRefs[identifierName] = {
1245
+ type: 'literal',
1246
+ value
1247
+ }
1248
+ } else
1249
+ if (isCapsuleInstance(value)) {
1250
+ ambientRefs[identifierName] = {
1251
+ type: 'capsule',
1252
+ value: value.toCapsuleReference()
1253
+ }
1254
+ } else {
1255
+ ambientRefs[identifierName] = {
1256
+ type: 'object',
1257
+ value
1258
+ }
1259
+ }
1260
+ }
1261
+ // If not provided, skip validation - it might be defined later or in outer scope
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ ts.forEachChild(node, visit)
1267
+ }
1268
+
1269
+ visit(call)
1270
+ return ambientRefs
1271
+ }
1272
+
1273
+ // Extract ambient references and validate them against runtime-provided values
1274
+ function extractAndValidateAmbientReferences(
1275
+ fn: ts.FunctionExpression | ts.ArrowFunction,
1276
+ sourceFile: ts.SourceFile,
1277
+ runtimeAmbientRefs: Record<string, any> | undefined,
1278
+ propName: string,
1279
+ spineContractName: string,
1280
+ importMap: Map<string, { importSpecifier: string, moduleUri: string }>,
1281
+ assignmentMap: Map<string, { importSpecifier: string, moduleUri: string }>
1282
+ ): Record<string, any> {
1283
+ // Build module-local functions map for checking
1284
+ const moduleLocalFunctions = new Map<string, ts.FunctionDeclaration>()
1285
+
1286
+ // Collect top-level module functions
1287
+ for (const statement of sourceFile.statements) {
1288
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
1289
+ moduleLocalFunctions.set(statement.name.text, statement)
1290
+ }
1291
+ }
1292
+
1293
+ const ambientRefs: Record<string, any> = {}
1294
+ const localIdentifiers = new Set<string>()
1295
+ const invocationParameters = new Set<string>()
1296
+
1297
+ // Find enclosing function and collect its parameters as invocation arguments and local functions
1298
+ let currentNode: ts.Node | undefined = fn
1299
+ let enclosingBlock: ts.Block | undefined
1300
+ while (currentNode) {
1301
+ if (ts.isFunctionExpression(currentNode) || ts.isArrowFunction(currentNode) || ts.isFunctionDeclaration(currentNode)) {
1302
+ // Skip the current function itself, look for parent functions
1303
+ if (currentNode !== fn) {
1304
+ for (const param of currentNode.parameters) {
1305
+ extractParameterNamesForInvocation(param.name, invocationParameters)
1306
+ }
1307
+ // Get the function body to collect local functions
1308
+ if (currentNode.body && ts.isBlock(currentNode.body)) {
1309
+ enclosingBlock = currentNode.body
1310
+ }
1311
+ break
1312
+ }
1313
+ }
1314
+ currentNode = currentNode.parent
1315
+ }
1316
+
1317
+ // Collect function declarations from the enclosing block
1318
+ if (enclosingBlock) {
1319
+ for (const statement of enclosingBlock.statements) {
1320
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
1321
+ moduleLocalFunctions.set(statement.name.text, statement)
1322
+ }
1323
+ }
1324
+ }
1325
+
1326
+ // Helper to extract parameter names for invocation detection
1327
+ function extractParameterNamesForInvocation(name: ts.BindingName, targetSet: Set<string>) {
1328
+ if (ts.isIdentifier(name)) {
1329
+ targetSet.add(name.text)
1330
+ } else if (ts.isObjectBindingPattern(name)) {
1331
+ for (const element of name.elements) {
1332
+ if (ts.isBindingElement(element)) {
1333
+ extractParameterNamesForInvocation(element.name, targetSet)
1334
+ }
1335
+ }
1336
+ } else if (ts.isArrayBindingPattern(name)) {
1337
+ for (const element of name.elements) {
1338
+ if (ts.isBindingElement(element)) {
1339
+ extractParameterNamesForInvocation(element.name, targetSet)
1340
+ }
1341
+ }
1342
+ }
1343
+ }
1344
+
1345
+ // Helper to extract identifiers from binding patterns
1346
+ function extractBindingIdentifiers(name: ts.BindingName) {
1347
+ if (ts.isIdentifier(name)) {
1348
+ localIdentifiers.add(name.text)
1349
+ } else if (ts.isArrayBindingPattern(name)) {
1350
+ for (const element of name.elements) {
1351
+ if (ts.isBindingElement(element)) {
1352
+ extractBindingIdentifiers(element.name)
1353
+ }
1354
+ }
1355
+ } else if (ts.isObjectBindingPattern(name)) {
1356
+ for (const element of name.elements) {
1357
+ if (ts.isBindingElement(element)) {
1358
+ extractBindingIdentifiers(element.name)
1359
+ }
1360
+ }
1361
+ }
1362
+ }
1363
+
1364
+ // Collect parameter names as local identifiers
1365
+ for (const param of fn.parameters) {
1366
+ extractBindingIdentifiers(param.name)
1367
+ }
1368
+
1369
+ // Traverse the function body to find identifiers
1370
+ function visit(node: ts.Node) {
1371
+ // Skip type nodes to avoid false positives from type annotations
1372
+ if (ts.isTypeNode(node)) {
1373
+ return
1374
+ }
1375
+
1376
+ // Track variable declarations within the function
1377
+ if (ts.isVariableDeclaration(node)) {
1378
+ extractBindingIdentifiers(node.name)
1379
+ }
1380
+
1381
+ // Track function declarations
1382
+ if (ts.isFunctionDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
1383
+ localIdentifiers.add(node.name.text)
1384
+ }
1385
+
1386
+ // Track named function expressions (e.g., function Counter() {})
1387
+ if (ts.isFunctionExpression(node) && node.name && ts.isIdentifier(node.name)) {
1388
+ localIdentifiers.add(node.name.text)
1389
+ }
1390
+
1391
+ // Track parameters from nested arrow functions and function expressions
1392
+ // This prevents false positives where callback parameters are treated as ambient references
1393
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
1394
+ for (const param of node.parameters) {
1395
+ extractBindingIdentifiers(param.name)
1396
+ }
1397
+ }
1398
+
1399
+ // Track for...in and for...of loop variables
1400
+ if (ts.isForInStatement(node) || ts.isForOfStatement(node)) {
1401
+ const initializer = node.initializer
1402
+ if (ts.isVariableDeclarationList(initializer)) {
1403
+ for (const declaration of initializer.declarations) {
1404
+ extractBindingIdentifiers(declaration.name)
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ // Check for identifiers that might be ambient references
1410
+ if (ts.isIdentifier(node)) {
1411
+ const identifierName = node.text
1412
+
1413
+ // Skip 'this' and other special keywords
1414
+ if (identifierName === 'this' || identifierName === 'undefined' || identifierName === 'null' || identifierName === 'arguments') {
1415
+ return
1416
+ }
1417
+
1418
+ // Skip if it's a local identifier (parameter or local variable)
1419
+ if (localIdentifiers.has(identifierName)) {
1420
+ return
1421
+ }
1422
+
1423
+ // Skip if it's a property access (e.g., this.username)
1424
+ const parent = node.parent
1425
+
1426
+ // Skip if it's part of import.meta (MetaProperty node)
1427
+ if (parent && parent.kind === ts.SyntaxKind.MetaProperty) {
1428
+ return
1429
+ }
1430
+
1431
+ if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) {
1432
+ return
1433
+ }
1434
+
1435
+ // Skip if it's a JSX attribute name
1436
+ if (parent && ts.isJsxAttribute(parent) && parent.name === node) {
1437
+ return
1438
+ }
1439
+
1440
+ // Skip if it's a property name in an object literal
1441
+ if (parent && ts.isPropertyAssignment(parent) && parent.name === node) {
1442
+ return
1443
+ }
1444
+
1445
+ // Skip if it's a parameter name (not a reference, but a declaration)
1446
+ if (parent && ts.isParameter(parent) && parent.name === node) {
1447
+ return
1448
+ }
1449
+
1450
+ // Check if we already added this reference
1451
+ if (ambientRefs[identifierName]) {
1452
+ return
1453
+ }
1454
+
1455
+ // Check if this is an imported identifier
1456
+ const importInfo = importMap.get(identifierName)
1457
+ if (importInfo) {
1458
+ // This is an import
1459
+ ambientRefs[identifierName] = {
1460
+ type: 'import',
1461
+ importSpecifier: importInfo.importSpecifier,
1462
+ moduleUri: importInfo.moduleUri
1463
+ }
1464
+ } else {
1465
+ // Check if this is an assigned variable from an imported function call
1466
+ const assignmentInfo = assignmentMap.get(identifierName)
1467
+ if (assignmentInfo) {
1468
+ // This is assigned from an imported function
1469
+ ambientRefs[identifierName] = {
1470
+ type: 'assigned',
1471
+ importSpecifier: assignmentInfo.importSpecifier,
1472
+ moduleUri: assignmentInfo.moduleUri
1473
+ }
1474
+ } else if (MODULE_GLOBAL_BUILTINS.has(identifierName)) {
1475
+ // This is a native JavaScript API (console, setTimeout, etc.)
1476
+ // Record it but don't require explicit declaration
1477
+ ambientRefs[identifierName] = {
1478
+ type: 'module-global'
1479
+ }
1480
+ } else if (invocationParameters.has(identifierName)) {
1481
+ // This is an invocation argument (parameter from enclosing function)
1482
+ ambientRefs[identifierName] = {
1483
+ type: 'invocation-argument',
1484
+ isEncapsulateExport: ENCAPSULATE_MODULE_EXPORTS.has(identifierName)
1485
+ }
1486
+ } else {
1487
+ // Check if it's a module-local function
1488
+ const funcDecl = moduleLocalFunctions.get(identifierName)
1489
+ if (funcDecl) {
1490
+ // Analyze if it's self-contained
1491
+ const dependencies = new Set<string>()
1492
+ const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
1493
+
1494
+ if (isContained) {
1495
+ // Mark as module-local
1496
+ ambientRefs[identifierName] = {
1497
+ type: 'module-local'
1498
+ }
1499
+ return
1500
+ }
1501
+ }
1502
+
1503
+ // Check if this is a JSX intrinsic element (like 'div', 'button', etc.) in a .tsx/.jsx file
1504
+ const fileName = sourceFile.fileName
1505
+ const isJsxFile = fileName.endsWith('.tsx') || fileName.endsWith('.jsx')
1506
+ if (isJsxFile && node.parent && (ts.isJsxOpeningElement(node.parent) || ts.isJsxSelfClosingElement(node.parent))) {
1507
+ // This is a JSX intrinsic element, record it but don't require validation
1508
+ ambientRefs[identifierName] = {
1509
+ type: 'jsx'
1510
+ }
1511
+ return
1512
+ }
1513
+
1514
+ // This is a literal ambient reference
1515
+ // Validate that the ambient reference is provided
1516
+ if (!runtimeAmbientRefs || !(identifierName in runtimeAmbientRefs)) {
1517
+ throw new Error(
1518
+ `Ambient reference '${identifierName}' used in property '${propName}' of spineContract '${spineContractName}' ` +
1519
+ `is not provided in encapsulate options.ambientReferences`
1520
+ )
1521
+ }
1522
+
1523
+ const value = runtimeAmbientRefs[identifierName]
1524
+
1525
+ // Validate that the value is a literal type or an instance
1526
+ if (!isLiteralType(value) && !isCapsuleInstance(value)) {
1527
+ throw new Error(
1528
+ `Ambient reference '${identifierName}' used in property '${propName}' of spineContract '${spineContractName}' ` +
1529
+ `must be a literal type (string, number, boolean, null, or undefined) or an instance, got: ${typeof value}`
1530
+ )
1531
+ }
1532
+
1533
+ // Add to ambient references with the value
1534
+ // For instances (capsules, functions, or non-literal objects), use '[instance]' as the value
1535
+ ambientRefs[identifierName] = {
1536
+ type: isLiteralType(value) ? 'literal' : 'instance',
1537
+ value: isLiteralType(value) ? value : '[instance]'
1538
+ }
1539
+ }
1540
+ }
1541
+ }
1542
+
1543
+ ts.forEachChild(node, visit)
1544
+ }
1545
+
1546
+ if (fn.body) {
1547
+ // Extract CSS imports from comments in function body
1548
+ // Pattern: /* import "./file.css"; */ or /* import './file.css' */ (with or without semicolon)
1549
+ const functionBodyText = fn.body.getText(sourceFile)
1550
+ const cssImportPattern = /\/\*\s*import\s+["']([^"']+\.css)["']\s*;?\s*\*\//g
1551
+ let match
1552
+
1553
+ while ((match = cssImportPattern.exec(functionBodyText)) !== null) {
1554
+ const cssPath = match[1]
1555
+
1556
+ // Add CSS import to ambient references if not already present
1557
+ if (!ambientRefs[cssPath]) {
1558
+ ambientRefs[cssPath] = {
1559
+ type: 'import',
1560
+ importSpecifier: `'${cssPath}'`,
1561
+ moduleUri: cssPath
1562
+ }
1563
+ }
1564
+ }
1565
+
1566
+ visit(fn.body)
1567
+ }
1568
+
1569
+ return ambientRefs
1570
+ }
1571
+
1572
+ // Check if a value is a literal type
1573
+ function isLiteralType(value: any): boolean {
1574
+ const type = typeof value
1575
+ return (
1576
+ type === 'string' ||
1577
+ type === 'number' ||
1578
+ type === 'boolean' ||
1579
+ value === null ||
1580
+ value === undefined
1581
+ )
1582
+ }
1583
+
1584
+ // Check if a value is an instance (object with capsuleSourceLineRef property)
1585
+ function isCapsuleInstance(value: any): boolean {
1586
+ return (
1587
+ typeof value === 'object' &&
1588
+ value !== null &&
1589
+ typeof value.toCapsuleReference === 'function'
1590
+ )
1591
+ }