@stream44.studio/encapsulate 0.4.0-rc.5

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