@stream44.studio/encapsulate 0.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1716 @@
1
+ import * as ts from 'typescript'
2
+ import { readFile, writeFile, mkdir, stat } from 'fs/promises'
3
+ import { join, dirname, relative, normalize, basename } from 'path'
4
+ import { merge } from '../encapsulate'
5
+
6
+
7
+ // Write mode determines CSS path resolution
8
+ type WriteMode = 'temp' | 'projection'
9
+
10
+ // Projection context holds all information needed for projecting a capsule
11
+ interface ProjectionContext {
12
+ capsule: any
13
+ customProjectionPath: string | null
14
+ mappedCapsules: Array<{ capsuleHash: string, projectionPath: string, capsule: any }>
15
+ cssFiles: Array<{ sourcePath: string, tempPath: string, projectionPath: string | null }>
16
+ }
17
+
18
+ function safeCapsuleName(name: string) {
19
+ // Strip leading @ and replace / with - for flat filenames
20
+ return name.replace(/^@/, '').replace(/[\/]/g, '-')
21
+ }
22
+
23
+ /**
24
+ * Constructs a cache file path for a capsule, using npm URI for external modules
25
+ * @param moduleFilepath - The module filepath (may start with ../)
26
+ * @param importStackLine - The import stack line number
27
+ * @param spineFilesystemRoot - The spine filesystem root
28
+ * @returns The cache file path to use
29
+ */
30
+ async function constructCacheFilePath(moduleFilepath: string, importStackLine: number, spineFilesystemRoot: string): Promise<string> {
31
+ if (moduleFilepath.startsWith('../')) {
32
+ // External module - construct npm URI
33
+ const absoluteFilepath = join(spineFilesystemRoot, moduleFilepath)
34
+ const npmUri = await constructNpmUriForCache(absoluteFilepath, spineFilesystemRoot)
35
+ if (npmUri) {
36
+ return `${npmUri}:${importStackLine}`
37
+ }
38
+ // Fallback to normalized path
39
+ return `${normalize(moduleFilepath).replace(/^\.\.\//, '').replace(/\.\.\//g, '')}:${importStackLine}`
40
+ }
41
+ // Internal module - use as-is
42
+ return `${moduleFilepath}:${importStackLine}`
43
+ }
44
+
45
+ /**
46
+ * Finds the nearest package.json and constructs an npm URI for cache files
47
+ * This matches the logic from static-analyzer.v0.ts
48
+ */
49
+ async function constructNpmUriForCache(absoluteFilepath: string, spineRoot: string): Promise<string | null> {
50
+ let currentDir = dirname(absoluteFilepath)
51
+ const maxDepth = 20 // Prevent infinite loops
52
+
53
+ for (let i = 0; i < maxDepth; i++) {
54
+ const packageJsonPath = join(currentDir, 'package.json')
55
+
56
+ try {
57
+ await stat(packageJsonPath)
58
+ // Found package.json, read it
59
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
60
+ const packageName = packageJson.name
61
+
62
+ if (!packageName) {
63
+ // No name in package.json, continue searching
64
+ currentDir = dirname(currentDir)
65
+ continue
66
+ }
67
+
68
+ // Get the relative path from the package root to the file
69
+ const relativeFromPackage = relative(currentDir, absoluteFilepath)
70
+
71
+ // Construct npm URI: packageName/relativePath
72
+ return `${packageName}/${relativeFromPackage}`
73
+ } catch (error) {
74
+ // package.json not found or not readable, go up one directory
75
+ const parentDir = dirname(currentDir)
76
+ if (parentDir === currentDir) {
77
+ // Reached filesystem root
78
+ break
79
+ }
80
+ currentDir = parentDir
81
+ }
82
+ }
83
+
84
+ return null
85
+ }
86
+
87
+ export function CapsuleModuleProjector({
88
+ spineStore,
89
+ projectionStore,
90
+ projectionCacheStore,
91
+ spineFilesystemRoot,
92
+ capsuleModuleProjectionPackage,
93
+ timing
94
+ }: {
95
+ spineStore: {
96
+ writeFile: (filepath: string, content: string) => Promise<void>,
97
+ getStats?: (filepath: string) => Promise<{ mtime: Date } | null>
98
+ },
99
+ projectionStore: {
100
+ writeFile: (filepath: string, content: string) => Promise<void>,
101
+ getStats?: (filepath: string) => Promise<{ mtime: Date } | null>
102
+ } | null,
103
+ projectionCacheStore?: {
104
+ writeFile?: (filepath: string, content: string) => Promise<void>,
105
+ readFile?: (filepath: string) => Promise<string | undefined>,
106
+ getStats?: (filepath: string) => Promise<{ mtime: Date } | null>
107
+ },
108
+ spineFilesystemRoot: string,
109
+ capsuleModuleProjectionPackage?: string,
110
+ timing?: { record: (step: string) => void, chalk?: any }
111
+ }) {
112
+
113
+ timing?.record('CapsuleModuleProjector: Initialized')
114
+
115
+
116
+ // Helper: Find custom projection path from property names starting with '/'
117
+ function findCustomProjectionPath(capsule: any, spineContractUri: string): string | null {
118
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
119
+ if (spineContract?.properties) {
120
+ for (const propName in spineContract.properties) {
121
+ if (propName.startsWith('/')) {
122
+ return propName.substring(1) // Remove leading '/'
123
+ }
124
+ }
125
+ }
126
+ return null
127
+ }
128
+
129
+ // Helper: Find mapped capsules that need custom projection
130
+ function findMappedCapsules(capsule: any, spineContractUri: string, capsules?: Record<string, any>): Array<{ capsuleHash: string, projectionPath: string, capsule: any }> {
131
+ const mapped: Array<{ capsuleHash: string, projectionPath: string, capsule: any }> = []
132
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
133
+
134
+ if (spineContract?.properties) {
135
+ for (const propName in spineContract.properties) {
136
+ if (propName.startsWith('/')) {
137
+ const prop = spineContract.properties[propName]
138
+ // Check if this is a Mapping type property
139
+ if (prop.type === 'CapsulePropertyTypes.Mapping') {
140
+ // First try to find the mapped capsule in ambient references
141
+ const ambientRefs = capsule.cst.source.ambientReferences || {}
142
+ for (const [refName, ref] of Object.entries(ambientRefs)) {
143
+ const refTyped = ref as any
144
+ if (refTyped.type === 'capsule') {
145
+ mapped.push({
146
+ capsuleHash: refTyped.value.capsuleSourceNameRefHash,
147
+ projectionPath: propName.substring(1), // Remove leading '/'
148
+ capsule: refTyped.value // Store the full capsule CST
149
+ })
150
+ }
151
+ }
152
+
153
+ // If not found in ambient refs, try string-based mapping
154
+ if (mapped.length === 0 && prop.valueExpression && capsules) {
155
+ const mappingValue = prop.valueExpression.replace(/['"]/g, '')
156
+
157
+ // Look through all capsules to find one that matches
158
+ for (const [key, potentialCapsule] of Object.entries(capsules)) {
159
+ if (potentialCapsule === capsule) continue
160
+
161
+ const mappedModulePath = potentialCapsule.cst?.source?.moduleFilepath
162
+
163
+ if (mappedModulePath && (
164
+ mappedModulePath === mappingValue ||
165
+ mappedModulePath.endsWith(mappingValue) ||
166
+ mappedModulePath.includes(mappingValue.replace('./', ''))
167
+ )) {
168
+ mapped.push({
169
+ capsuleHash: potentialCapsule.cst.capsuleSourceNameRefHash,
170
+ projectionPath: propName.substring(1),
171
+ capsule: potentialCapsule.cst
172
+ })
173
+ break
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ // Also check nested property contracts that start with '#'
180
+ if (propName.startsWith('#')) {
181
+ const propContract = spineContract.properties[propName] as any
182
+ if (propContract?.properties) {
183
+ for (const nestedPropName in propContract.properties) {
184
+ if (nestedPropName.startsWith('/')) {
185
+ const nestedProp = propContract.properties[nestedPropName]
186
+ // Check if this is a Mapping type property
187
+ if (nestedProp.type === 'CapsulePropertyTypes.Mapping') {
188
+ // First try to find the mapped capsule in ambient references
189
+ const ambientRefs = capsule.cst.source.ambientReferences || {}
190
+ let foundInAmbient = false
191
+ for (const [refName, ref] of Object.entries(ambientRefs)) {
192
+ const refTyped = ref as any
193
+ if (refTyped.type === 'capsule') {
194
+ mapped.push({
195
+ capsuleHash: refTyped.value.capsuleSourceNameRefHash,
196
+ projectionPath: nestedPropName.substring(1), // Remove leading '/'
197
+ capsule: refTyped.value // Store the full capsule CST
198
+ })
199
+ foundInAmbient = true
200
+ }
201
+ }
202
+
203
+ // If not found in ambient refs, try string-based mapping
204
+ if (!foundInAmbient && nestedProp.valueExpression && capsules) {
205
+ const mappingValue = nestedProp.valueExpression.replace(/['"]/g, '')
206
+
207
+ // Look through all capsules to find one that matches
208
+ for (const [key, potentialCapsule] of Object.entries(capsules)) {
209
+ if (potentialCapsule === capsule) continue
210
+
211
+ const mappedModulePath = potentialCapsule.cst?.source?.moduleFilepath
212
+
213
+ if (mappedModulePath && (
214
+ mappedModulePath === mappingValue ||
215
+ mappedModulePath.endsWith(mappingValue) ||
216
+ mappedModulePath.includes(mappingValue.replace('./', ''))
217
+ )) {
218
+ mapped.push({
219
+ capsuleHash: potentialCapsule.cst.capsuleSourceNameRefHash,
220
+ projectionPath: nestedPropName.substring(1),
221
+ capsule: potentialCapsule.cst
222
+ })
223
+ break
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ return mapped
236
+ }
237
+
238
+ // Helper: Traverse CST to collect all capsule URIs from mappings and property contracts
239
+ function collectAllCapsuleUris(capsule: any, spineContractUri: string): Set<string> {
240
+ const uris = new Set<string>()
241
+
242
+ function traverseProperties(properties: any) {
243
+ if (!properties) return
244
+
245
+ for (const [key, prop] of Object.entries(properties)) {
246
+ const propTyped = prop as any
247
+
248
+ // Check for Mapping type
249
+ if (propTyped.type === 'CapsulePropertyTypes.Mapping') {
250
+ if (propTyped.valueExpression) {
251
+ // Extract URI from value expression (e.g., "@stream44.studio/encapsulate/structs/Capsule")
252
+ const match = propTyped.valueExpression.match(/["']([^"']+)["']/)
253
+ if (match && match[1].startsWith('@')) {
254
+ uris.add(match[1])
255
+ }
256
+ }
257
+ }
258
+
259
+ // Recursively traverse nested properties
260
+ if (propTyped.properties) {
261
+ traverseProperties(propTyped.properties)
262
+ }
263
+ }
264
+ }
265
+
266
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
267
+ if (spineContract?.properties) {
268
+ traverseProperties(spineContract.properties)
269
+ }
270
+
271
+ return uris
272
+ }
273
+
274
+ // Helper: Check if capsule has solidjs.com/standalone property
275
+ function hasSolidJsProperty(capsule: any, spineContractUri: string): boolean {
276
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
277
+ // Check both top-level and nested under '#' property contract
278
+ const topLevelProps = spineContract?.properties || {}
279
+ const nestedProps = spineContract?.properties?.['#']?.properties || {}
280
+
281
+ // Check for solidjs.com/standalone specifically
282
+ for (const key of Object.keys(topLevelProps)) {
283
+ if (key === 'solidjs.com/standalone') return true
284
+ }
285
+ for (const key of Object.keys(nestedProps)) {
286
+ if (key === 'solidjs.com/standalone') return true
287
+ }
288
+ return false
289
+ }
290
+
291
+ // Helper: Check if capsule has encapsulate.dev/standalone property (with optional suffix)
292
+ function hasStandaloneProperty(capsule: any, spineContractUri: string): boolean {
293
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
294
+ // Check both top-level and nested under '#' property contract
295
+ const topLevelProps = spineContract?.properties || {}
296
+ const nestedProps = spineContract?.properties?.['#']?.properties || {}
297
+
298
+ // Check for exact match or with suffix
299
+ for (const key of Object.keys(topLevelProps)) {
300
+ if (key === 'encapsulate.dev/standalone' || key.startsWith('encapsulate.dev/standalone/')) return true
301
+ }
302
+ for (const key of Object.keys(nestedProps)) {
303
+ if (key === 'encapsulate.dev/standalone' || key.startsWith('encapsulate.dev/standalone/')) return true
304
+ }
305
+ return false
306
+ }
307
+
308
+ // Helper: Extract SolidJS component function from capsule definition
309
+ function extractSolidJsComponent(capsule: any, spineContractUri: string): string | null {
310
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
311
+
312
+ // Check nested under '#' property contract first, looking for solidjs.com/standalone
313
+ const nestedProps = spineContract?.properties?.['#']?.properties || {}
314
+ const topLevelProps = spineContract?.properties || {}
315
+
316
+ let solidjsProp = null
317
+ for (const key of Object.keys(nestedProps)) {
318
+ if (key === 'solidjs.com/standalone') {
319
+ solidjsProp = nestedProps[key]
320
+ break
321
+ }
322
+ }
323
+ if (!solidjsProp) {
324
+ for (const key of Object.keys(topLevelProps)) {
325
+ if (key === 'solidjs.com/standalone') {
326
+ solidjsProp = topLevelProps[key]
327
+ break
328
+ }
329
+ }
330
+ }
331
+
332
+ if (!solidjsProp || solidjsProp.type !== 'CapsulePropertyTypes.Function') {
333
+ return null
334
+ }
335
+
336
+ // Extract the value expression which contains the component function
337
+ const valueExpression = solidjsProp.valueExpression
338
+ if (!valueExpression) return null
339
+
340
+ // The value expression is: "function (this: any): Function {\n return function ComponentName() { ... }\n}"
341
+ // We need to extract the inner function after "return "
342
+ // Use a more flexible regex that handles multiline and varying whitespace
343
+ const match = valueExpression.match(/return\s+(function\s+\w*\s*\([^)]*\)\s*\{[\s\S]*)\s*\}\s*$/m)
344
+ if (match) {
345
+ // Clean up the extracted function - remove extra indentation
346
+ let extracted = match[1].trim()
347
+ // Add back the closing brace if it was removed
348
+ if (!extracted.endsWith('}')) {
349
+ extracted += '\n}'
350
+ }
351
+ // Remove leading indentation from each line
352
+ const lines = extracted.split('\n')
353
+ const minIndent = lines
354
+ .filter(line => line.trim().length > 0)
355
+ .map(line => line.match(/^(\s*)/)?.[1].length || 0)
356
+ .reduce((min, indent) => Math.min(min, indent), Infinity)
357
+
358
+ if (minIndent > 0 && minIndent !== Infinity) {
359
+ extracted = lines.map(line => line.substring(minIndent)).join('\n')
360
+ }
361
+
362
+ return extracted
363
+ }
364
+
365
+ return null
366
+ }
367
+
368
+ // Helper: Extract standalone function from capsule definition
369
+ function extractStandaloneFunction(capsule: any, spineContractUri: string): string | null {
370
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
371
+
372
+ // Check nested under '#' property contract first, looking for encapsulate.dev/standalone or encapsulate.dev/standalone/*
373
+ const nestedProps = spineContract?.properties?.['#']?.properties || {}
374
+ const topLevelProps = spineContract?.properties || {}
375
+
376
+ let standaloneProp = null
377
+ for (const key of Object.keys(nestedProps)) {
378
+ if (key === 'encapsulate.dev/standalone' || key.startsWith('encapsulate.dev/standalone/')) {
379
+ standaloneProp = nestedProps[key]
380
+ break
381
+ }
382
+ }
383
+ if (!standaloneProp) {
384
+ for (const key of Object.keys(topLevelProps)) {
385
+ if (key === 'encapsulate.dev/standalone' || key.startsWith('encapsulate.dev/standalone/')) {
386
+ standaloneProp = topLevelProps[key]
387
+ break
388
+ }
389
+ }
390
+ }
391
+
392
+ if (!standaloneProp || standaloneProp.type !== 'CapsulePropertyTypes.Function') {
393
+ return null
394
+ }
395
+
396
+ // Extract the value expression which contains the standalone function
397
+ const valueExpression = standaloneProp.valueExpression
398
+ if (!valueExpression) return null
399
+
400
+ // The value expression is: "function (this: any): Function {\n return function testFunction() { ... }\n}"
401
+ // We need to extract the inner function after "return "
402
+ const match = valueExpression.match(/return\s+(function\s+\w*\s*\([^)]*\)\s*\{[\s\S]*)\s*\}\s*$/m)
403
+ if (match) {
404
+ // Clean up the extracted function - remove extra indentation
405
+ let extracted = match[1].trim()
406
+ // Add back the closing brace if it was removed
407
+ if (!extracted.endsWith('}')) {
408
+ extracted += '\n}'
409
+ }
410
+ // Remove leading indentation from each line
411
+ const lines = extracted.split('\n')
412
+ const minIndent = lines
413
+ .filter(line => line.trim().length > 0)
414
+ .map(line => line.match(/^(\s*)/)?.[1].length || 0)
415
+ .reduce((min, indent) => Math.min(min, indent), Infinity)
416
+
417
+ if (minIndent > 0 && minIndent !== Infinity) {
418
+ extracted = lines.map(line => line.substring(minIndent)).join('\n')
419
+ }
420
+
421
+ return extracted
422
+ }
423
+
424
+ return null
425
+ }
426
+
427
+ // Helper: Build projection context
428
+ function buildProjectionContext(capsule: any, spineContractUri: string, capsules?: Record<string, any>): ProjectionContext {
429
+ return {
430
+ capsule,
431
+ customProjectionPath: findCustomProjectionPath(capsule, spineContractUri),
432
+ mappedCapsules: findMappedCapsules(capsule, spineContractUri, capsules),
433
+ cssFiles: []
434
+ }
435
+ }
436
+
437
+ // Helper: Build capsule snapshot for a referenced capsule
438
+ async function buildCapsuleSnapshotForReference(ref: any, capsules: Record<string, any> | undefined, spineContractUri: string): Promise<any> {
439
+ // Look up the full capsule from the registry to get capsule name
440
+ let capsule = null
441
+ if (capsules) {
442
+ for (const [key, cap] of Object.entries(capsules)) {
443
+ if (cap.cst?.capsuleSourceNameRefHash === ref.value.capsuleSourceNameRefHash) {
444
+ capsule = cap
445
+ break
446
+ }
447
+ }
448
+ }
449
+
450
+ if (!capsule) throw new Error(`Could not locate capsule definition!`)
451
+
452
+ const sourceExtension = capsule.cst.source.moduleFilepath?.endsWith('.tsx') ? '.tsx' : '.ts'
453
+
454
+ let projectedPath: string
455
+
456
+ // Use capsuleName if it exists (WITHOUT line number), otherwise use capsuleSourceUriLineRef (WITH line number)
457
+ if (capsule.cst.source.capsuleName) {
458
+ // Use capsuleName as the path - NO line number
459
+ let capsuleNamePath = capsule.cst.source.capsuleName
460
+ // Strip @ prefix
461
+ if (capsuleNamePath.startsWith('@')) {
462
+ capsuleNamePath = capsuleNamePath.substring(1)
463
+ }
464
+ // Just capsuleName + extension, NO line number
465
+ projectedPath = `.~caps/${capsuleNamePath}${sourceExtension}`
466
+ } else if (capsule.cst.capsuleSourceUriLineRef) {
467
+ // Use capsuleSourceUriLineRef which already has format: uri:line
468
+ let uriPath = capsule.cst.capsuleSourceUriLineRef
469
+ // Strip @ prefix
470
+ if (uriPath.startsWith('@')) {
471
+ uriPath = uriPath.substring(1)
472
+ }
473
+ // Add extension after the line number: uri:line.ext
474
+ projectedPath = `.~caps/${uriPath}${sourceExtension}`
475
+ } else {
476
+ // Fallback to hash if neither exists
477
+ projectedPath = `.~caps/${capsule.cst.capsuleSourceNameRefHash.substring(0, 8)}${sourceExtension}`
478
+ }
479
+
480
+ return {
481
+ spineContracts: {
482
+ [spineContractUri]: {
483
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {
484
+ capsuleSourceNameRefHash: capsule.cst.capsuleSourceNameRefHash,
485
+ projectedCapsuleFilepath: projectedPath,
486
+ capsuleName: capsule.cst.source.capsuleName || ''
487
+ }
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ const projectCapsule = async ({ capsule, capsules, snapshotValues, spineContractUri }: { capsule: any, capsules?: Record<string, any>, snapshotValues?: any, spineContractUri: string }): Promise<boolean> => {
494
+
495
+ timing?.record(`Projector: Start projection for ${capsule.cst.source.moduleFilepath}`)
496
+
497
+ // Only project capsules that have the Capsule struct property
498
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
499
+ if (!spineContract?.properties?.['#@stream44.studio/encapsulate/structs/Capsule.v0']) {
500
+ return false
501
+ }
502
+
503
+ if (projectionCacheStore?.getStats && spineStore?.getStats) {
504
+ try {
505
+ const cacheFilePath = await constructCacheFilePath(
506
+ capsule.cst.source.moduleFilepath,
507
+ capsule.cst.source.importStackLine,
508
+ spineFilesystemRoot
509
+ )
510
+ const cacheFilename = `${cacheFilePath}.projection.json`
511
+ const [sourceStats, cacheStats] = await Promise.all([
512
+ spineStore.getStats(capsule.cst.source.moduleFilepath),
513
+ projectionCacheStore.getStats(cacheFilename)
514
+ ])
515
+
516
+ // If cache exists and is newer than source, verify projected files exist before using cache
517
+ if (sourceStats && cacheStats && cacheStats.mtime >= sourceStats.mtime && projectionCacheStore.readFile) {
518
+ const cacheContent = await projectionCacheStore.readFile(cacheFilename)
519
+ if (cacheContent) {
520
+ const cachedData = JSON.parse(cacheContent)
521
+
522
+ // Check if ALL projection files exist (main + mapped)
523
+ const context = buildProjectionContext(capsule, spineContractUri, capsules)
524
+ let allProjectedFilesExist = true
525
+
526
+ // First, check if the MAIN projection file exists
527
+ const mainProjectionPath = cachedData.snapshotData?.spineContracts?.[spineContractUri]?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.projectedCapsuleFilepath
528
+ if (mainProjectionPath && projectionStore?.getStats) {
529
+ const mainStats = await projectionStore.getStats(mainProjectionPath)
530
+ if (!mainStats) {
531
+ allProjectedFilesExist = false
532
+ }
533
+ }
534
+
535
+ // Then check if mapped capsule projection paths exist
536
+ if (allProjectedFilesExist) {
537
+ for (const mapped of context.mappedCapsules) {
538
+ if (projectionStore?.getStats) {
539
+ const stats = await projectionStore.getStats(mapped.projectionPath)
540
+ if (!stats) {
541
+ allProjectedFilesExist = false
542
+ break
543
+ }
544
+ }
545
+ }
546
+ }
547
+
548
+ if (allProjectedFilesExist) {
549
+ // Restore snapshotValues from cache
550
+ Object.assign(snapshotValues, merge(snapshotValues, cachedData.snapshotData))
551
+ timing?.record(`Projector: Cache HIT for ${capsule.cst.source.moduleFilepath}`)
552
+ return true
553
+ } else {
554
+ timing?.record(timing?.chalk?.yellow?.(`Projector: Cache INVALID (projected files missing) for ${capsule.cst.source.moduleFilepath}`))
555
+ }
556
+ }
557
+ }
558
+
559
+ // Recursively project all mapped capsules first
560
+ if (capsules) {
561
+ // Look through all capsules to find ones that are mapped by this capsule
562
+ for (const [key, potentialMappedCapsule] of Object.entries(capsules)) {
563
+ // Skip the current capsule
564
+ if (potentialMappedCapsule === capsule) continue
565
+
566
+ // Check if this capsule has the Capsule struct (meaning it should be projected)
567
+ if (potentialMappedCapsule.cst?.spineContracts?.[spineContractUri]?.properties?.['#@stream44.studio/encapsulate/structs/Capsule.v0']) {
568
+ // Check if this capsule's moduleFilepath is referenced in any mapping property
569
+ const mappedModulePath = potentialMappedCapsule.cst.source.moduleFilepath
570
+
571
+ for (const [propContractKey, propContract] of Object.entries(spineContract.properties)) {
572
+ if (propContractKey.startsWith('#') && (propContract as any).properties) {
573
+ for (const [propName, propDef] of Object.entries((propContract as any).properties)) {
574
+ const prop = propDef as any
575
+ if (prop.type === 'CapsulePropertyTypes.Mapping') {
576
+ const mappingValue = prop.valueExpression?.replace(/['"]/g, '')
577
+ // Check if this mapping points to the potential mapped capsule
578
+ if (mappingValue && mappedModulePath && (
579
+ mappedModulePath.endsWith(mappingValue + '.ts') ||
580
+ mappedModulePath.endsWith(mappingValue + '.tsx') ||
581
+ mappedModulePath.includes(mappingValue.replace('./', ''))
582
+ )) {
583
+ await projectCapsule({ capsule: potentialMappedCapsule, capsules, snapshotValues, spineContractUri })
584
+ }
585
+ }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ timing?.record(timing?.chalk?.red?.(`Projector: Cache MISS for ${capsule.cst.source.moduleFilepath}`))
593
+ } catch (error) {
594
+ // Cache miss or error, proceed with projection
595
+ }
596
+ }
597
+
598
+ const context = buildProjectionContext(capsule, spineContractUri, capsules)
599
+
600
+ // Set projectedCapsuleFilepath in snapshotValues using buildCapsuleSnapshotForReference
601
+ const snapshotData = await buildCapsuleSnapshotForReference({ value: capsule.cst }, capsules, spineContractUri)
602
+ Object.assign(snapshotValues, merge(snapshotValues, snapshotData))
603
+
604
+ // Retrieve the filepath from snapshotValues
605
+ const filepath = snapshotValues.spineContracts[spineContractUri]['#@stream44.studio/encapsulate/structs/Capsule.v0'].projectedCapsuleFilepath
606
+
607
+ // Helper: Rewrite capsule expression to inline CST using regex
608
+ function rewriteCapsuleExpressionWithCST(targetCapsule: any): string {
609
+ let expression = targetCapsule.cst.source.capsuleExpression || ''
610
+
611
+ const cstJson = JSON.stringify(targetCapsule.cst, null, 4)
612
+ const crtJson = JSON.stringify(targetCapsule.crt || {}, null, 4)
613
+ const moduleFilepath = targetCapsule.cst.source.moduleFilepath
614
+ const importStackLine = targetCapsule.cst.source.importStackLine
615
+
616
+ // Replace importMeta: import.meta with moduleFilepath: '...'
617
+ expression = expression.replace(
618
+ /importMeta:\s*import\.meta/g,
619
+ `moduleFilepath: '${moduleFilepath}'`
620
+ )
621
+
622
+ // Replace importStack: makeImportStack() with importStackLine: ..., crt: {...}, cst: {...}
623
+ expression = expression.replace(
624
+ /importStack:\s*makeImportStack\(\)/g,
625
+ `importStackLine: ${importStackLine}, crt: ${crtJson}, cst: ${cstJson}`
626
+ )
627
+
628
+ // Remove ambientReferences from the expression since they're handled separately
629
+ // Match: ambientReferences: { ... } including multi-line with proper brace matching
630
+ expression = expression.replace(
631
+ /,?\s*ambientReferences:\s*\{[\s\S]*?\}/gm,
632
+ ''
633
+ )
634
+
635
+ return expression
636
+ }
637
+
638
+ let capsuleExpression = rewriteCapsuleExpressionWithCST(capsule)
639
+
640
+ let ambientReferences = capsule.cst.source.ambientReferences || {}
641
+
642
+ // Helper: Process CSS files for a capsule with mode-aware path resolution
643
+ async function processCssFiles(
644
+ targetCapsule: any,
645
+ mode: WriteMode,
646
+ targetPath: string | null,
647
+ sourceModuleFilepath: string
648
+ ): Promise<Record<string, string>> {
649
+ const cssImportMapping: Record<string, string> = {}
650
+ const capsuleAmbientRefs = targetCapsule.source?.ambientReferences || {}
651
+
652
+ for (const [name, ref] of Object.entries(capsuleAmbientRefs)) {
653
+ const refTyped = ref as any
654
+ if (refTyped.type === 'import' && refTyped.moduleUri.endsWith('.css')) {
655
+ const originalCssPath = refTyped.moduleUri
656
+ const cssBasename = basename(originalCssPath, '.css')
657
+
658
+ // Generate CSS paths based on mode
659
+ let cssFilePath: string
660
+ let cssImportPath: string
661
+
662
+ if (mode === 'temp') {
663
+ // Temp mode: use hash-based naming
664
+ cssFilePath = `./.~capsule-${targetCapsule.capsuleSourceNameRefHash.substring(0, 8)}-${cssBasename}.css`
665
+ cssImportPath = cssFilePath
666
+ } else {
667
+ // Projection mode: use target path-based naming
668
+ if (targetPath) {
669
+ const targetDir = dirname(targetPath)
670
+ const targetBase = basename(targetPath, '.tsx').replace(/\.ts$/, '')
671
+ cssFilePath = `${targetDir}/${targetBase}-${cssBasename}.css`
672
+ // Import path should be relative with ./ prefix
673
+ cssImportPath = `./${targetBase}-${cssBasename}.css`
674
+ } else {
675
+ // Fallback to temp naming
676
+ cssFilePath = `./.~capsule-${targetCapsule.capsuleSourceNameRefHash.substring(0, 8)}-${cssBasename}.css`
677
+ cssImportPath = cssFilePath
678
+ }
679
+ }
680
+
681
+ // Read source CSS file
682
+ try {
683
+ const sourceModuleDir = dirname(join(spineFilesystemRoot, sourceModuleFilepath))
684
+ const sourceCssPath = join(sourceModuleDir, originalCssPath)
685
+ const cssContent = await readFile(sourceCssPath, 'utf-8')
686
+
687
+ // Write CSS file based on mode
688
+ if (mode === 'temp') {
689
+ // Write relative to source module directory
690
+ const sourceDir = dirname(sourceModuleFilepath)
691
+ const relativeCssPath = join(sourceDir, cssFilePath)
692
+ await spineStore.writeFile(relativeCssPath, cssContent)
693
+ } else if (projectionStore) {
694
+ await projectionStore.writeFile(cssFilePath, cssContent)
695
+ }
696
+
697
+ cssImportMapping[originalCssPath] = cssImportPath
698
+ } catch (error) {
699
+ console.warn(`Warning: Could not copy CSS file ${originalCssPath}:`, error)
700
+ }
701
+ }
702
+ }
703
+
704
+ return cssImportMapping
705
+ }
706
+
707
+ // Process CSS files for temp mode
708
+ const cssImportMapping = await processCssFiles(
709
+ capsule.cst,
710
+ 'temp',
711
+ null,
712
+ capsule.cst.source.moduleFilepath
713
+ )
714
+
715
+ // Exclude makeImportStack from ambient references since it's being replaced with importStackLine
716
+ if (ambientReferences['makeImportStack']) {
717
+ ambientReferences = { ...ambientReferences }
718
+ delete ambientReferences['makeImportStack']
719
+ }
720
+
721
+ const importStatements = Object.entries(ambientReferences)
722
+ .map(([name, ref]: [string, any]) => {
723
+ if (ref.type === 'import') {
724
+ // Check if this is a CSS import (moduleUri ends with .css)
725
+ if (ref.moduleUri.endsWith('.css')) {
726
+ // Use the mapped CSS path if available
727
+ const cssPath = cssImportMapping[ref.moduleUri] || ref.moduleUri
728
+ return `import '${cssPath}'`
729
+ }
730
+ return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
731
+ }
732
+ if (ref.type === 'assigned') {
733
+ // If the assignment comes from a spine-factory module, import from encapsulate.ts instead
734
+ if (ref.moduleUri.includes('/spine-factories/')) {
735
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
736
+ }
737
+ }
738
+ if (ref.type === 'invocation-argument') {
739
+ // Only import if it's an available encapsulate export
740
+ if (ref.isEncapsulateExport) {
741
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
742
+ }
743
+ }
744
+ return ''
745
+ })
746
+ .filter(Boolean)
747
+ .join('\n')
748
+
749
+ const literalReferences = Object.entries(ambientReferences)
750
+ .map(([name, ref]: [string, any]) => {
751
+ if (ref.type === 'literal') {
752
+ return `const ${name} = ${JSON.stringify(ref.value)}`
753
+ }
754
+ return ''
755
+ })
756
+ .filter(Boolean)
757
+ .join('\n')
758
+
759
+ const instanceInitializationsPromises = Object.entries(ambientReferences)
760
+ .map(async ([name, ref]: [string, any]) => {
761
+ if (ref.type === 'capsule') {
762
+ const capsuleSnapshot = await buildCapsuleSnapshotForReference(ref, capsules, spineContractUri)
763
+ return `const ${name}_fn = await loadCapsule({ capsuleSnapshot: ${JSON.stringify(capsuleSnapshot, null, 4)} })\n const ${name} = await ${name}_fn({ encapsulate, loadCapsule })`
764
+ }
765
+ return ''
766
+ })
767
+ const instanceInitializations = (await Promise.all(instanceInitializationsPromises))
768
+ .filter(Boolean)
769
+ .join('\n ')
770
+
771
+ // Extract module-local code for functions marked as 'module-local'
772
+ const moduleLocalCode = capsule.cst.source.moduleLocalCode || {}
773
+ const moduleLocalFunctions = Object.entries(moduleLocalCode)
774
+ .map(([name, code]: [string, any]) => code)
775
+ .filter(Boolean)
776
+ .join('\n\n')
777
+
778
+ const allStatements = [importStatements, literalReferences, moduleLocalFunctions].filter(Boolean).join('\n')
779
+
780
+ // Check if this capsule has a solidjs.com or standalone property
781
+ const hasSolidJs = hasSolidJsProperty(capsule, spineContractUri)
782
+ const hasStandalone = hasStandaloneProperty(capsule, spineContractUri)
783
+ const needsRuntime = hasSolidJs || hasStandalone
784
+
785
+ // Determine which solid-js imports are needed (avoid duplicates with ambient references)
786
+ const existingSolidJsImports = new Set<string>()
787
+ for (const [name, ref] of Object.entries(ambientReferences)) {
788
+ const refTyped = ref as any
789
+ if (refTyped.type === 'import' && refTyped.moduleUri === 'solid-js') {
790
+ // Parse existing imports from solid-js
791
+ const match = refTyped.importSpecifier?.match(/\{([^}]+)\}/)
792
+ if (match) {
793
+ match[1].split(',').forEach((imp: string) => existingSolidJsImports.add(imp.trim()))
794
+ }
795
+ }
796
+ }
797
+
798
+ const neededSolidJsImports = ['createSignal', 'onMount', 'Show'].filter(imp => !existingSolidJsImports.has(imp))
799
+ const solidJsImport = hasSolidJs && neededSolidJsImports.length > 0 ? `import { ${neededSolidJsImports.join(', ')} } from 'solid-js'\n` : ''
800
+
801
+ // Add runtime imports for SolidJS and standalone functions
802
+ const runtimeImport = needsRuntime ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane.v0'\n${solidJsImport}` : ''
803
+
804
+ // Generate default export based on capsule type
805
+ let defaultExport = ''
806
+ if (hasSolidJs) {
807
+ // Generate a wrapper that sets up runtime and exports the SolidJS component
808
+ const capsuleSourceLineRef = capsule.cst.capsuleSourceLineRef
809
+ const solidjsComponent = extractSolidJsComponent(capsule, spineContractUri)
810
+ if (solidjsComponent) {
811
+ // Collect all capsule URIs from CST (mappings and property contracts)
812
+ const allCapsuleUris = collectAllCapsuleUris(capsule, spineContractUri)
813
+
814
+ // Also collect from ambient references and build import paths from snapshots
815
+ const capsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
816
+ for (const [name, ref] of Object.entries(ambientReferences)) {
817
+ const refTyped = ref as any
818
+ if (refTyped.type === 'capsule') {
819
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
820
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.capsuleName
821
+ const projectedFilepath = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.projectedCapsuleFilepath
822
+
823
+ if (capsuleName && projectedFilepath) {
824
+ allCapsuleUris.add(capsuleName)
825
+
826
+ // Build import path from projected filepath
827
+ const importName = `_capsule_${capsuleName.replace(/[^a-zA-Z0-9]/g, '_')}`
828
+ // Remove .~caps/ prefix and strip extension
829
+ const importPath = projectedFilepath.replace(/^\.~caps\//, '').replace(/\.(ts|tsx)$/, '')
830
+
831
+ capsuleDeps.push({ uri: capsuleName, importName, importPath })
832
+ }
833
+ }
834
+ }
835
+
836
+ // Generate static imports for all capsule dependencies
837
+ // Compute relative path from projected file to .~caps directory
838
+ let importPrefix: string
839
+ if (capsuleModuleProjectionPackage) {
840
+ importPrefix = capsuleModuleProjectionPackage
841
+ } else {
842
+ const projectedFileDir = dirname(filepath)
843
+ const capsDir = '.~caps'
844
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
845
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
846
+ }
847
+ const capsuleImports = capsuleDeps.map(dep =>
848
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
849
+ ).join('\n')
850
+
851
+ // Generate capsules map
852
+ const capsulesMapEntries = capsuleDeps.map(dep =>
853
+ ` '${dep.uri}': ${dep.importName}`
854
+ ).join(',\n')
855
+
856
+ defaultExport = `
857
+ ${capsuleImports}
858
+
859
+ // Set up runtime for browser execution
860
+ const sourceSpine: { encapsulate?: any } = {}
861
+
862
+ // Map of statically imported capsules
863
+ const capsulesMap: Record<string, any> = {
864
+ ${capsulesMapEntries}
865
+ }
866
+
867
+ // Helper to import and instantiate a capsule from the capsules map
868
+ const importCapsule = async (uri: string) => {
869
+ const capsuleModule = capsulesMap[uri]
870
+ if (!capsuleModule) {
871
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
872
+ }
873
+ const capsule = await capsuleModule.capsule({
874
+ encapsulate: sourceSpine.encapsulate,
875
+ loadCapsule
876
+ })
877
+ return capsule
878
+ }
879
+
880
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
881
+ // Return the capsule function from this projected file
882
+ if (capsuleSourceLineRef === '${capsuleSourceLineRef}') {
883
+ return capsule
884
+ }
885
+
886
+ // Use capsuleName directly if provided
887
+ if (capsuleName) {
888
+ return await importCapsule(capsuleName)
889
+ }
890
+
891
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
892
+ }
893
+
894
+ const spineContractOpts = {
895
+ spineFilesystemRoot: '.',
896
+ resolve: async (uri: string) => uri,
897
+ importCapsule
898
+ }
899
+
900
+ const runtimeSpineContracts = {
901
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
902
+ }
903
+
904
+ const snapshot = {
905
+ capsules: {
906
+ ['${capsuleSourceLineRef}']: {
907
+ spineContracts: {}
908
+ }
909
+ }
910
+ }
911
+
912
+ // Export wrapper function that initializes runtime and returns component
913
+ export default function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
914
+ const [component, setComponent] = createSignal(null)
915
+
916
+ onMount(async () => {
917
+ // Add onMembraneEvent to spine contract opts - use provided or default logger
918
+ const defaultMembraneLogger = (event: any) => {
919
+ console.log('[Membrane Event]', event)
920
+ }
921
+ const opts = {
922
+ ...spineContractOpts,
923
+ onMembraneEvent: onMembraneEvent || defaultMembraneLogger
924
+ }
925
+ const contracts = {
926
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(opts)
927
+ }
928
+
929
+ const { encapsulate, capsules } = await Spine({
930
+ spineFilesystemRoot: '.',
931
+ spineContracts: contracts
932
+ })
933
+
934
+ sourceSpine.encapsulate = encapsulate
935
+
936
+ const capsuleInstance = await capsule({ encapsulate, loadCapsule })
937
+
938
+ const { run } = await SpineRuntime({
939
+ spineFilesystemRoot: '.',
940
+ spineContracts: contracts,
941
+ snapshot,
942
+ loadCapsule
943
+ })
944
+
945
+ const Component = await run({}, async ({ apis }) => {
946
+ const capsuleApi = apis['${capsuleSourceLineRef}']
947
+ const solidjsKey = Object.keys(capsuleApi).find(k => k === 'solidjs.com/standalone')
948
+ if (!solidjsKey) throw new Error('solidjs.com/standalone property not found')
949
+ return capsuleApi[solidjsKey]()
950
+ })
951
+
952
+ setComponent(() => Component)
953
+ })
954
+
955
+ // Return the wrapper function itself, not call it
956
+ const WrapperComponent = () => {
957
+ const Component = component()
958
+ return Show({ when: Component, children: (Component) => Component() })
959
+ }
960
+
961
+ return WrapperComponent
962
+ }
963
+ `
964
+ }
965
+ } else if (hasStandalone) {
966
+ // Generate a wrapper function that directly invokes the standalone function
967
+ const capsuleSourceLineRef = capsule.cst.capsuleSourceLineRef
968
+
969
+ // Collect all capsule URIs from CST (mappings and property contracts)
970
+ const allCapsuleUris = collectAllCapsuleUris(capsule, spineContractUri)
971
+
972
+ // Also collect from ambient references
973
+ for (const [name, ref] of Object.entries(ambientReferences)) {
974
+ const refTyped = ref as any
975
+ if (refTyped.type === 'capsule') {
976
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
977
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.capsuleName
978
+ if (capsuleName) {
979
+ allCapsuleUris.add(capsuleName)
980
+ }
981
+ }
982
+ }
983
+
984
+ // Build import paths from snapshots for ALL collected URIs
985
+ const capsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
986
+ for (const uri of allCapsuleUris) {
987
+ // Find the capsule in the registry and build its snapshot
988
+ let capsuleRef = null
989
+ if (capsules) {
990
+ for (const [key, cap] of Object.entries(capsules)) {
991
+ const capCapsuleName = cap.cst?.source?.capsuleName
992
+ if (capCapsuleName === uri) {
993
+ capsuleRef = { type: 'capsule', value: cap.cst }
994
+ break
995
+ }
996
+ }
997
+ }
998
+
999
+ if (capsuleRef) {
1000
+ const snapshot = await buildCapsuleSnapshotForReference(capsuleRef, capsules, spineContractUri)
1001
+ const projectedFilepath = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.projectedCapsuleFilepath
1002
+
1003
+ if (projectedFilepath) {
1004
+ // Build import path from projected filepath
1005
+ const importName = `_capsule_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`
1006
+ // Remove .~caps/ prefix and strip extension
1007
+ const importPath = projectedFilepath.replace(/^\.~caps\//, '').replace(/\.(ts|tsx)$/, '')
1008
+
1009
+ capsuleDeps.push({ uri, importName, importPath })
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ // Generate static imports for all capsule dependencies
1015
+ // Compute relative path from projected file to .~caps directory
1016
+ let importPrefix: string
1017
+ if (capsuleModuleProjectionPackage) {
1018
+ importPrefix = capsuleModuleProjectionPackage
1019
+ } else {
1020
+ const projectedFileDir = dirname(filepath)
1021
+ const capsDir = '.~caps'
1022
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
1023
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
1024
+ }
1025
+ const capsuleImports = capsuleDeps.map(dep =>
1026
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
1027
+ ).join('\n')
1028
+
1029
+ // Generate capsules map
1030
+ const capsulesMapEntries = capsuleDeps.map(dep =>
1031
+ ` '${dep.uri}': ${dep.importName}`
1032
+ ).join(',\n')
1033
+
1034
+ defaultExport = `
1035
+ ${capsuleImports}
1036
+
1037
+ // Export standalone function - directly invoke the capsule API
1038
+ export default async function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
1039
+ // Use sourceSpine pattern like the factory to provide real encapsulate
1040
+ const sourceSpine: { encapsulate?: any } = {}
1041
+
1042
+ // Map of statically imported capsules
1043
+ const capsulesMap: Record<string, any> = {
1044
+ ${capsulesMapEntries}
1045
+ }
1046
+
1047
+ // Helper to import and instantiate a capsule from the capsules map
1048
+ const importCapsule = async (uri: string) => {
1049
+ const capsuleModule = capsulesMap[uri]
1050
+ if (!capsuleModule) {
1051
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
1052
+ }
1053
+ const capsule = await capsuleModule.capsule({
1054
+ encapsulate: sourceSpine.encapsulate,
1055
+ loadCapsule
1056
+ })
1057
+ return capsule
1058
+ }
1059
+
1060
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
1061
+ // Return the capsule function from this projected file
1062
+ if (capsuleSourceLineRef === '${capsuleSourceLineRef}') {
1063
+ return capsule
1064
+ }
1065
+
1066
+ // Use capsuleName directly if provided
1067
+ if (capsuleName) {
1068
+ return await importCapsule(capsuleName)
1069
+ }
1070
+
1071
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
1072
+ }
1073
+
1074
+ const spineContractOpts = {
1075
+ spineFilesystemRoot: '.',
1076
+ resolve: async (uri: string) => uri,
1077
+ importCapsule,
1078
+ ...(onMembraneEvent ? { onMembraneEvent } : {})
1079
+ }
1080
+
1081
+ const runtimeSpineContracts = {
1082
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
1083
+ }
1084
+
1085
+ const snapshot = {
1086
+ capsules: {
1087
+ ['${capsuleSourceLineRef}']: {
1088
+ spineContracts: {}
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ const { run, encapsulate } = await SpineRuntime({
1094
+ spineFilesystemRoot: '.',
1095
+ spineContracts: runtimeSpineContracts,
1096
+ snapshot,
1097
+ loadCapsule
1098
+ })
1099
+
1100
+ // Populate sourceSpine.encapsulate so imported capsules can use it
1101
+ sourceSpine.encapsulate = encapsulate
1102
+
1103
+ const result = await run({}, async ({ apis }) => {
1104
+ const capsuleApi = apis['${capsuleSourceLineRef}']
1105
+ const standaloneKey = Object.keys(capsuleApi).find(k => k === 'encapsulate.dev/standalone' || k.startsWith('encapsulate.dev/standalone/'))
1106
+ if (!standaloneKey) throw new Error('encapsulate.dev/standalone property not found')
1107
+ const standaloneFunc = capsuleApi[standaloneKey]()
1108
+ return standaloneFunc()
1109
+ })
1110
+
1111
+ return result
1112
+ }
1113
+ `
1114
+ }
1115
+
1116
+ // Prepare literal references with proper indentation
1117
+ const indentedLiteralRefs = literalReferences ? literalReferences.split('\n').map(line => line ? ` ${line}` : '').join('\n') + '\n' : ''
1118
+ const indentedInstanceInits = instanceInitializations ? ' ' + instanceInitializations.split('\n').join('\n ') + '\n' : ''
1119
+
1120
+ const fileContent = `${runtimeImport}${importStatements}
1121
+ ${moduleLocalFunctions}
1122
+
1123
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1124
+ ${indentedLiteralRefs}${indentedInstanceInits} return ${capsuleExpression}
1125
+ }
1126
+ capsule['#'] = ${JSON.stringify(capsule.cst.source.capsuleName || '')}
1127
+ ${defaultExport}
1128
+ `
1129
+ // Write to projection store
1130
+ if (projectionStore) {
1131
+ await projectionStore.writeFile(filepath, fileContent)
1132
+ }
1133
+
1134
+ // If this capsule has a custom projection path, also write there
1135
+ if (context.customProjectionPath) {
1136
+ // Process CSS files for projection mode
1137
+ const projectionCssMapping = await processCssFiles(
1138
+ capsule.cst,
1139
+ 'projection',
1140
+ context.customProjectionPath,
1141
+ capsule.cst.source.moduleFilepath
1142
+ )
1143
+
1144
+ // Update import statements to use projection CSS paths
1145
+ const projectionImportStatements = Object.entries(ambientReferences)
1146
+ .map(([name, ref]: [string, any]) => {
1147
+ if (ref.type === 'import') {
1148
+ if (ref.moduleUri.endsWith('.css')) {
1149
+ const cssPath = projectionCssMapping[ref.moduleUri] || ref.moduleUri
1150
+ return `import '${cssPath}'`
1151
+ }
1152
+ return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
1153
+ }
1154
+ if (ref.type === 'assigned') {
1155
+ if (ref.moduleUri.includes('/spine-factories/')) {
1156
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1157
+ }
1158
+ }
1159
+ if (ref.type === 'invocation-argument') {
1160
+ // Only import if it's an available encapsulate export
1161
+ if (ref.isEncapsulateExport) {
1162
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1163
+ }
1164
+ }
1165
+ return ''
1166
+ })
1167
+ .filter(Boolean)
1168
+ .join('\n')
1169
+
1170
+ const projectionAllStatements = [projectionImportStatements, literalReferences].filter(Boolean).join('\n')
1171
+ const projectionFileContent = `${runtimeImport}${projectionAllStatements}
1172
+
1173
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1174
+ ${instanceInitializations}
1175
+ return ${capsuleExpression}
1176
+ }
1177
+ capsule['#'] = ${JSON.stringify(capsule.cst.source.capsuleName || '')}
1178
+ ${defaultExport}
1179
+ `
1180
+ if (projectionStore) {
1181
+ await projectionStore.writeFile(context.customProjectionPath, projectionFileContent)
1182
+ }
1183
+ }
1184
+
1185
+ // Write mapped capsules to their projection paths
1186
+ for (const mapped of context.mappedCapsules) {
1187
+ // Get the full mapped capsule from the registry using its hash
1188
+ if (!capsules) {
1189
+ console.warn(`Warning: Cannot write mapped capsule - no capsules registry provided`)
1190
+ continue
1191
+ }
1192
+
1193
+ // Find the capsule in the registry by matching the hash
1194
+ let mappedCapsule = null
1195
+ for (const [key, cap] of Object.entries(capsules)) {
1196
+ if (cap.cst?.capsuleSourceNameRefHash === mapped.capsuleHash) {
1197
+ mappedCapsule = cap
1198
+ break
1199
+ }
1200
+ }
1201
+
1202
+ if (!mappedCapsule) {
1203
+ console.warn(`Warning: Mapped capsule with hash ${mapped.capsuleHash} not found in registry`)
1204
+ continue
1205
+ }
1206
+
1207
+ // Process CSS files for the mapped capsule in projection mode
1208
+ const mappedCssMapping = await processCssFiles(
1209
+ mappedCapsule.cst,
1210
+ 'projection',
1211
+ mapped.projectionPath,
1212
+ mappedCapsule.cst.source.moduleFilepath
1213
+ )
1214
+
1215
+ // Get ambient references for the mapped capsule
1216
+ const mappedAmbientRefs = mappedCapsule.cst.source?.ambientReferences || {}
1217
+
1218
+ // Generate import statements with projection CSS paths
1219
+ const mappedImportStatements = Object.entries(mappedAmbientRefs)
1220
+ .map(([name, ref]: [string, any]) => {
1221
+ if (ref.type === 'import') {
1222
+ if (ref.moduleUri.endsWith('.css')) {
1223
+ const cssPath = mappedCssMapping[ref.moduleUri] || ref.moduleUri
1224
+ return `import '${cssPath}'`
1225
+ }
1226
+ return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
1227
+ }
1228
+ if (ref.type === 'assigned') {
1229
+ if (ref.moduleUri.includes('/spine-factories/')) {
1230
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1231
+ }
1232
+ }
1233
+ if (ref.type === 'invocation-argument') {
1234
+ // Only import if it's an available encapsulate export
1235
+ if (ref.isEncapsulateExport) {
1236
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1237
+ }
1238
+ }
1239
+ return ''
1240
+ })
1241
+ .filter(Boolean)
1242
+ .join('\n')
1243
+
1244
+ const mappedLiteralReferences = Object.entries(mappedAmbientRefs)
1245
+ .map(([name, ref]: [string, any]) => {
1246
+ if (ref.type === 'literal') {
1247
+ return `const ${name} = ${JSON.stringify(ref.value)}`
1248
+ }
1249
+ return ''
1250
+ })
1251
+ .filter(Boolean)
1252
+ .join('\n')
1253
+
1254
+ const mappedInstanceInitializationsPromises = Object.entries(mappedAmbientRefs)
1255
+ .map(async ([name, ref]: [string, any]) => {
1256
+ if (ref.type === 'capsule') {
1257
+ const capsuleSnapshot = await buildCapsuleSnapshotForReference(ref, capsules, spineContractUri)
1258
+ return `const ${name}_fn = await loadCapsule({ capsuleSnapshot: ${JSON.stringify(capsuleSnapshot, null, 4)} })\n const ${name} = await ${name}_fn({ encapsulate, loadCapsule })`
1259
+ }
1260
+ return ''
1261
+ })
1262
+ const mappedInstanceInitializations = (await Promise.all(mappedInstanceInitializationsPromises))
1263
+ .filter(Boolean)
1264
+ .join('\n ')
1265
+
1266
+ // Extract module-local code for mapped capsule
1267
+ const mappedModuleLocalCode = mappedCapsule.cst.source?.moduleLocalCode || {}
1268
+ const mappedModuleLocalFunctions = Object.entries(mappedModuleLocalCode)
1269
+ .map(([name, code]: [string, any]) => code)
1270
+ .filter(Boolean)
1271
+ .join('\n\n')
1272
+
1273
+ const mappedAllStatements = [mappedImportStatements, mappedLiteralReferences, mappedModuleLocalFunctions].filter(Boolean).join('\n')
1274
+
1275
+ // Check if mapped capsule has solidjs.com or encapsulate.dev/standalone property
1276
+ const mappedHasSolidJs = hasSolidJsProperty(mappedCapsule, spineContractUri)
1277
+ const mappedHasStandalone = hasStandaloneProperty(mappedCapsule, spineContractUri)
1278
+ const mappedNeedsRuntime = mappedHasSolidJs || mappedHasStandalone
1279
+
1280
+ // Determine which solid-js imports are needed for mapped capsule
1281
+ const mappedExistingSolidJsImports = new Set<string>()
1282
+ for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
1283
+ const refTyped = ref as any
1284
+ if (refTyped.type === 'import' && refTyped.moduleUri === 'solid-js') {
1285
+ const match = refTyped.importSpecifier?.match(/\{([^}]+)\}/)
1286
+ if (match) {
1287
+ match[1].split(',').forEach((imp: string) => mappedExistingSolidJsImports.add(imp.trim()))
1288
+ }
1289
+ }
1290
+ }
1291
+
1292
+ const mappedNeededSolidJsImports = ['createSignal', 'onMount', 'Show'].filter(imp => !mappedExistingSolidJsImports.has(imp))
1293
+ const mappedSolidJsImport = mappedHasSolidJs && mappedNeededSolidJsImports.length > 0 ? `import { ${mappedNeededSolidJsImports.join(', ')} } from 'solid-js'\n` : ''
1294
+
1295
+ // Rewrite the mapped capsule expression to include CST (reuse the same function)
1296
+ const mappedCapsuleExpression = rewriteCapsuleExpressionWithCST(mappedCapsule)
1297
+
1298
+ // Add runtime imports for SolidJS and standalone functions
1299
+ const mappedRuntimeImport = mappedNeedsRuntime ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane.v0'\n${mappedSolidJsImport}` : ''
1300
+
1301
+ let mappedDefaultExport = ''
1302
+ if (mappedHasSolidJs) {
1303
+ // Generate a wrapper that sets up runtime and exports the SolidJS component
1304
+ const mappedCapsuleSourceLineRef = mappedCapsule.cst.capsuleSourceLineRef
1305
+ const solidjsComponent = extractSolidJsComponent(mappedCapsule, spineContractUri)
1306
+ if (solidjsComponent) {
1307
+ // Collect all capsule URIs from CST (mappings and property contracts)
1308
+ const allMappedCapsuleUris = collectAllCapsuleUris(mappedCapsule, spineContractUri)
1309
+
1310
+ // Also collect from ambient references
1311
+ for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
1312
+ const refTyped = ref as any
1313
+ if (refTyped.type === 'capsule') {
1314
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
1315
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.capsuleName
1316
+ if (capsuleName) {
1317
+ allMappedCapsuleUris.add(capsuleName)
1318
+ }
1319
+ }
1320
+ }
1321
+
1322
+ // Build capsule dependencies array
1323
+ const mappedCapsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
1324
+ for (const uri of allMappedCapsuleUris) {
1325
+ const importName = `_capsule_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`
1326
+ // Strip leading @ from URI to avoid double @ in import paths
1327
+ const importPath = uri.startsWith('@') ? uri.substring(1) : uri
1328
+ mappedCapsuleDeps.push({ uri, importName, importPath })
1329
+ }
1330
+
1331
+ // Generate static imports for all capsule dependencies
1332
+ // Compute relative path from projected file to .~caps directory
1333
+ let importPrefix: string
1334
+ if (capsuleModuleProjectionPackage) {
1335
+ importPrefix = capsuleModuleProjectionPackage
1336
+ } else {
1337
+ const projectedFileDir = dirname(mapped.projectionPath)
1338
+ const capsDir = '.~caps'
1339
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
1340
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
1341
+ }
1342
+ const mappedCapsuleImports = mappedCapsuleDeps.map(dep =>
1343
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
1344
+ ).join('\n')
1345
+
1346
+ // Generate capsules map
1347
+ const mappedCapsulesMapEntries = mappedCapsuleDeps.map(dep =>
1348
+ ` '${dep.uri}': ${dep.importName}`
1349
+ ).join(',\n')
1350
+
1351
+ mappedDefaultExport = `
1352
+ ${mappedCapsuleImports}
1353
+
1354
+ // Set up runtime for browser execution
1355
+ const sourceSpine: { encapsulate?: any } = {}
1356
+
1357
+ // Map of statically imported capsules
1358
+ const capsulesMap: Record<string, any> = {
1359
+ ${mappedCapsulesMapEntries}
1360
+ }
1361
+
1362
+ // Helper to import and instantiate a capsule from the capsules map
1363
+ const importCapsule = async (uri: string) => {
1364
+ const capsuleModule = capsulesMap[uri]
1365
+ if (!capsuleModule) {
1366
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
1367
+ }
1368
+ const capsule = await capsuleModule.capsule({
1369
+ encapsulate: sourceSpine.encapsulate,
1370
+ loadCapsule
1371
+ })
1372
+ return capsule
1373
+ }
1374
+
1375
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
1376
+ // Return the capsule function from this projected file
1377
+ if (capsuleSourceLineRef === '${mappedCapsuleSourceLineRef}') {
1378
+ return capsule
1379
+ }
1380
+
1381
+ // Use capsuleName directly if provided
1382
+ if (capsuleName) {
1383
+ return await importCapsule(capsuleName)
1384
+ }
1385
+
1386
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
1387
+ }
1388
+
1389
+ const spineContractOpts = {
1390
+ spineFilesystemRoot: '.',
1391
+ resolve: async (uri: string) => uri,
1392
+ importCapsule
1393
+ }
1394
+
1395
+ const runtimeSpineContracts = {
1396
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
1397
+ }
1398
+
1399
+ const snapshot = {
1400
+ capsules: {
1401
+ ['${mappedCapsuleSourceLineRef}']: {
1402
+ spineContracts: {}
1403
+ }
1404
+ }
1405
+ }
1406
+
1407
+ // Export wrapper function that initializes runtime and returns component
1408
+ export default function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
1409
+ const [component, setComponent] = createSignal(null)
1410
+
1411
+ onMount(async () => {
1412
+ // Add onMembraneEvent to spine contract opts - use provided or default logger
1413
+ const defaultMembraneLogger = (event: any) => {
1414
+ console.log('[Membrane Event]', event.type, event)
1415
+ }
1416
+ const opts = {
1417
+ ...spineContractOpts,
1418
+ onMembraneEvent: onMembraneEvent || defaultMembraneLogger
1419
+ }
1420
+ const contracts = {
1421
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(opts)
1422
+ }
1423
+
1424
+ const { encapsulate, capsules } = await Spine({
1425
+ spineFilesystemRoot: '.',
1426
+ spineContracts: contracts
1427
+ })
1428
+
1429
+ sourceSpine.encapsulate = encapsulate
1430
+
1431
+ const capsuleInstance = await capsule({ encapsulate, loadCapsule })
1432
+
1433
+ const { run } = await SpineRuntime({
1434
+ spineFilesystemRoot: '.',
1435
+ spineContracts: contracts,
1436
+ snapshot,
1437
+ loadCapsule
1438
+ })
1439
+
1440
+ const Component = await run({}, async ({ apis }) => {
1441
+ const capsuleApi = apis['${mappedCapsuleSourceLineRef}']
1442
+ const solidjsKey = Object.keys(capsuleApi).find(k => k === 'solidjs.com/standalone')
1443
+ if (!solidjsKey) throw new Error('solidjs.com/standalone property not found')
1444
+ return capsuleApi[solidjsKey]()
1445
+ })
1446
+
1447
+ setComponent(() => Component)
1448
+ })
1449
+
1450
+ return () => {
1451
+ const Component = component()
1452
+ return Show({ when: Component, children: (Component) => Component() })
1453
+ }
1454
+ }
1455
+ `
1456
+ }
1457
+ } else if (mappedHasStandalone) {
1458
+ // Generate a wrapper function that directly invokes the standalone function
1459
+ const mappedCapsuleSourceLineRef = mappedCapsule.cst.capsuleSourceLineRef
1460
+
1461
+ // Collect all capsule URIs from CST (mappings and property contracts)
1462
+ const allMappedCapsuleUris = collectAllCapsuleUris(mappedCapsule, spineContractUri)
1463
+
1464
+ // Also collect from ambient references
1465
+ for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
1466
+ const refTyped = ref as any
1467
+ if (refTyped.type === 'capsule') {
1468
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
1469
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.capsuleName
1470
+ if (capsuleName) {
1471
+ allMappedCapsuleUris.add(capsuleName)
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ // Build capsule dependencies array
1477
+ const mappedCapsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
1478
+ for (const uri of allMappedCapsuleUris) {
1479
+ const importName = `_capsule_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`
1480
+ // Strip leading @ from URI to avoid double @ in import paths
1481
+ const importPath = uri.startsWith('@') ? uri.substring(1) : uri
1482
+ mappedCapsuleDeps.push({ uri, importName, importPath })
1483
+ }
1484
+
1485
+ // Generate static imports for all capsule dependencies
1486
+ // Compute relative path from projected file to .~caps directory
1487
+ let importPrefix: string
1488
+ if (capsuleModuleProjectionPackage) {
1489
+ importPrefix = capsuleModuleProjectionPackage
1490
+ } else {
1491
+ const projectedFileDir = dirname(mapped.projectionPath)
1492
+ const capsDir = '.~caps'
1493
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
1494
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
1495
+ }
1496
+ const mappedCapsuleImports = mappedCapsuleDeps.map(dep =>
1497
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
1498
+ ).join('\n')
1499
+
1500
+ // Generate capsules map
1501
+ const mappedCapsulesMapEntries = mappedCapsuleDeps.map(dep =>
1502
+ ` '${dep.uri}': ${dep.importName}`
1503
+ ).join(',\n')
1504
+
1505
+ mappedDefaultExport = `
1506
+ ${mappedCapsuleImports}
1507
+
1508
+ // Export standalone function - directly invoke the capsule API
1509
+ export default async function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
1510
+ // Use sourceSpine pattern like the factory to provide real encapsulate
1511
+ const sourceSpine: { encapsulate?: any } = {}
1512
+
1513
+ // Map of statically imported capsules
1514
+ const capsulesMap: Record<string, any> = {
1515
+ ${mappedCapsulesMapEntries}
1516
+ }
1517
+
1518
+ // Helper to import and instantiate a capsule from the capsules map
1519
+ const importCapsule = async (uri: string) => {
1520
+ const capsuleModule = capsulesMap[uri]
1521
+ if (!capsuleModule) {
1522
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
1523
+ }
1524
+ const capsule = await capsuleModule.capsule({
1525
+ encapsulate: sourceSpine.encapsulate,
1526
+ loadCapsule
1527
+ })
1528
+ return capsule
1529
+ }
1530
+
1531
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
1532
+ // Return the capsule function from this projected file
1533
+ if (capsuleSourceLineRef === '${mappedCapsuleSourceLineRef}') {
1534
+ return capsule
1535
+ }
1536
+
1537
+ // Use capsuleName directly if provided
1538
+ if (capsuleName) {
1539
+ return await importCapsule(capsuleName)
1540
+ }
1541
+
1542
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
1543
+ }
1544
+
1545
+ const spineContractOpts = {
1546
+ spineFilesystemRoot: '.',
1547
+ resolve: async (uri: string) => uri,
1548
+ importCapsule,
1549
+ ...(onMembraneEvent ? { onMembraneEvent } : {})
1550
+ }
1551
+
1552
+ const runtimeSpineContracts = {
1553
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
1554
+ }
1555
+
1556
+ const snapshot = {
1557
+ capsules: {
1558
+ ['${mappedCapsuleSourceLineRef}']: {
1559
+ spineContracts: {}
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ const { run, encapsulate } = await SpineRuntime({
1565
+ spineFilesystemRoot: '.',
1566
+ spineContracts: runtimeSpineContracts,
1567
+ snapshot,
1568
+ loadCapsule
1569
+ })
1570
+
1571
+ // Populate sourceSpine.encapsulate so imported capsules can use it
1572
+ sourceSpine.encapsulate = encapsulate
1573
+
1574
+ const result = await run({}, async ({ apis }) => {
1575
+ const capsuleApi = apis['${mappedCapsuleSourceLineRef}']
1576
+ const standaloneKey = Object.keys(capsuleApi).find(k => k === 'encapsulate.dev/standalone' || k.startsWith('encapsulate.dev/standalone/'))
1577
+ if (!standaloneKey) throw new Error('encapsulate.dev/standalone property not found')
1578
+ const standaloneFunc = capsuleApi[standaloneKey]()
1579
+ return standaloneFunc()
1580
+ })
1581
+
1582
+ return result
1583
+ }
1584
+ `
1585
+ }
1586
+
1587
+ const mappedFileContent = `${mappedRuntimeImport}${mappedAllStatements}
1588
+
1589
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1590
+ ${mappedInstanceInitializations}
1591
+ return ${mappedCapsuleExpression}
1592
+ }
1593
+ capsule['#'] = ${JSON.stringify(mappedCapsule.cst.source.capsuleName || '')}
1594
+ ${mappedDefaultExport}
1595
+ `
1596
+
1597
+ // Write mapped capsule to projection path
1598
+ if (projectionStore) {
1599
+ await projectionStore.writeFile(mapped.projectionPath, mappedFileContent)
1600
+ }
1601
+ }
1602
+
1603
+ // Write projection cache metadata for main capsule
1604
+ if (projectionCacheStore?.writeFile) {
1605
+ try {
1606
+ const cacheFilePath = await constructCacheFilePath(
1607
+ capsule.cst.source.moduleFilepath,
1608
+ capsule.cst.source.importStackLine,
1609
+ spineFilesystemRoot
1610
+ )
1611
+ const cacheFilename = `${cacheFilePath}.projection.json`
1612
+ const cacheData = {
1613
+ snapshotData: await buildCapsuleSnapshotForReference({ value: capsule.cst }, capsules, spineContractUri)
1614
+ }
1615
+ await projectionCacheStore.writeFile(cacheFilename, JSON.stringify(cacheData, null, 2))
1616
+ } catch (error) {
1617
+ // Cache write error, continue without failing
1618
+ console.warn(`Warning: Failed to write projection cache for ${capsule.cst.source.moduleFilepath}:`, error)
1619
+ }
1620
+ }
1621
+
1622
+ // Write projection cache AND project ALL capsules in the registry to .~caps
1623
+ // This includes struct definitions, property contract capsules, and any other capsules
1624
+ if (projectionCacheStore?.writeFile && capsules) {
1625
+ for (const [capsuleKey, registryCapsule] of Object.entries(capsules)) {
1626
+ // Skip if this is the main capsule we already processed
1627
+ if (registryCapsule === capsule) {
1628
+ continue
1629
+ }
1630
+
1631
+ // Skip if this capsule doesn't have the required CST structure
1632
+ if (!registryCapsule.cst?.source?.moduleFilepath || !registryCapsule.cst?.source?.importStackLine) {
1633
+ continue
1634
+ }
1635
+
1636
+ try {
1637
+ // Write projection cache
1638
+ const capsuleCacheFilePath = await constructCacheFilePath(
1639
+ registryCapsule.cst.source.moduleFilepath,
1640
+ registryCapsule.cst.source.importStackLine,
1641
+ spineFilesystemRoot
1642
+ )
1643
+ const capsuleCacheFilename = `${capsuleCacheFilePath}.projection.json`
1644
+ const capsuleCacheData = {
1645
+ snapshotData: await buildCapsuleSnapshotForReference({ value: registryCapsule.cst }, capsules, spineContractUri)
1646
+ }
1647
+ await projectionCacheStore.writeFile(capsuleCacheFilename, JSON.stringify(capsuleCacheData, null, 2))
1648
+
1649
+ // Also project the capsule to .~caps
1650
+ const projectedPath = capsuleCacheData.snapshotData.spineContracts[spineContractUri]['#@stream44.studio/encapsulate/structs/Capsule.v0'].projectedCapsuleFilepath
1651
+
1652
+ // Generate the capsule file content with proper imports and ambient reference loading
1653
+ const capsuleExpression = rewriteCapsuleExpressionWithCST(registryCapsule)
1654
+
1655
+ // Get ambient references for this capsule
1656
+ const capsuleAmbientRefs = registryCapsule.cst.source?.ambientReferences || {}
1657
+
1658
+ // Generate literal references
1659
+ const capsuleLiteralRefs = Object.entries(capsuleAmbientRefs)
1660
+ .map(([name, ref]: [string, any]) => {
1661
+ if (ref.type === 'literal') {
1662
+ return `const ${name} = ${JSON.stringify(ref.value)}`
1663
+ }
1664
+ return ''
1665
+ })
1666
+ .filter(Boolean)
1667
+ .join('\n')
1668
+
1669
+ // Generate instance initializations for ambient references
1670
+ const capsuleInstanceInitPromises = Object.entries(capsuleAmbientRefs)
1671
+ .map(async ([name, ref]: [string, any]) => {
1672
+ if (ref.type === 'capsule') {
1673
+ const capsuleSnapshot = await buildCapsuleSnapshotForReference(ref, capsules, spineContractUri)
1674
+ return `const ${name}_fn = await loadCapsule({ capsuleSnapshot: ${JSON.stringify(capsuleSnapshot, null, 4)} })\n const ${name} = await ${name}_fn({ encapsulate, loadCapsule })`
1675
+ }
1676
+ return ''
1677
+ })
1678
+ const capsuleInstanceInits = (await Promise.all(capsuleInstanceInitPromises))
1679
+ .filter(Boolean)
1680
+ .join('\n ')
1681
+
1682
+ // Add necessary imports
1683
+ const imports = `import { CapsulePropertyTypes } from '@stream44.studio/encapsulate/encapsulate'
1684
+ import { makeImportStack } from '@stream44.studio/encapsulate/encapsulate'
1685
+ `
1686
+
1687
+ // Get the capsule name for the assignment
1688
+ const capsuleName = registryCapsule.cst.source.capsuleName || ''
1689
+
1690
+ // Combine literal refs and instance inits with proper indentation
1691
+ const indentedLiterals = capsuleLiteralRefs ? capsuleLiteralRefs.split('\n').map(line => ` ${line}`).join('\n') + '\n' : ''
1692
+ const indentedInits = capsuleInstanceInits ? ' ' + capsuleInstanceInits + '\n' : ''
1693
+
1694
+ const capsuleFileContent = `${imports}
1695
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1696
+ ${indentedLiterals}${indentedInits} return ${capsuleExpression}
1697
+ }
1698
+ capsule['#'] = ${JSON.stringify(capsuleName)}
1699
+ `
1700
+
1701
+ if (projectionStore) {
1702
+ await projectionStore.writeFile(projectedPath, capsuleFileContent)
1703
+ }
1704
+ } catch (error) {
1705
+ console.warn(`Warning: Failed to write projection cache for capsule ${registryCapsule.cst.source.moduleFilepath}:`, error)
1706
+ }
1707
+ }
1708
+ }
1709
+
1710
+ return true
1711
+ }
1712
+
1713
+ return {
1714
+ projectCapsule
1715
+ }
1716
+ }