@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.
- package/README.md +21 -0
- package/package.json +21 -0
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +1716 -0
- package/src/encapsulate.ts +662 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +624 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +28 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +290 -0
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +299 -0
- package/src/spine-factories/TimingObserver.ts +26 -0
- package/src/static-analyzer.v0.ts +1591 -0
- package/structs/Capsule.v0.ts +22 -0
- package/tsconfig.json +30 -0
|
@@ -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
|
+
}
|