@stream44.studio/encapsulate 0.4.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1725 @@
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': {
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, projectingCapsules }: { capsule: any, capsules?: Record<string, any>, snapshotValues?: any, spineContractUri: string, projectingCapsules?: Set<string> }): Promise<boolean> => {
494
+
495
+ // Check for circular dependency - if already projecting this capsule, skip
496
+ if (projectingCapsules) {
497
+ const capsuleId = capsule.cst?.capsuleSourceNameRefHash || capsule.capsuleSourceLineRef
498
+ if (projectingCapsules.has(capsuleId)) {
499
+ return true
500
+ }
501
+ projectingCapsules.add(capsuleId)
502
+ }
503
+
504
+ timing?.record(`Projector: Start projection for ${capsule.cst.source.moduleFilepath}`)
505
+
506
+ // Only project capsules that have the Capsule struct property
507
+ const spineContract = capsule.cst.spineContracts[spineContractUri]
508
+ if (!spineContract?.properties?.['#@stream44.studio/encapsulate/structs/Capsule']) {
509
+ return false
510
+ }
511
+
512
+ if (projectionCacheStore?.getStats && spineStore?.getStats) {
513
+ try {
514
+ const cacheFilePath = await constructCacheFilePath(
515
+ capsule.cst.source.moduleFilepath,
516
+ capsule.cst.source.importStackLine,
517
+ spineFilesystemRoot
518
+ )
519
+ const cacheFilename = `${cacheFilePath}.projection.json`
520
+ const [sourceStats, cacheStats] = await Promise.all([
521
+ spineStore.getStats(capsule.cst.source.moduleFilepath),
522
+ projectionCacheStore.getStats(cacheFilename)
523
+ ])
524
+
525
+ // If cache exists and is newer than source, verify projected files exist before using cache
526
+ if (sourceStats && cacheStats && cacheStats.mtime >= sourceStats.mtime && projectionCacheStore.readFile) {
527
+ const cacheContent = await projectionCacheStore.readFile(cacheFilename)
528
+ if (cacheContent) {
529
+ const cachedData = JSON.parse(cacheContent)
530
+
531
+ // Check if ALL projection files exist (main + mapped)
532
+ const context = buildProjectionContext(capsule, spineContractUri, capsules)
533
+ let allProjectedFilesExist = true
534
+
535
+ // First, check if the MAIN projection file exists
536
+ const mainProjectionPath = cachedData.snapshotData?.spineContracts?.[spineContractUri]?.['#@stream44.studio/encapsulate/structs/Capsule']?.projectedCapsuleFilepath
537
+ if (mainProjectionPath && projectionStore?.getStats) {
538
+ const mainStats = await projectionStore.getStats(mainProjectionPath)
539
+ if (!mainStats) {
540
+ allProjectedFilesExist = false
541
+ }
542
+ }
543
+
544
+ // Then check if mapped capsule projection paths exist
545
+ if (allProjectedFilesExist) {
546
+ for (const mapped of context.mappedCapsules) {
547
+ if (projectionStore?.getStats) {
548
+ const stats = await projectionStore.getStats(mapped.projectionPath)
549
+ if (!stats) {
550
+ allProjectedFilesExist = false
551
+ break
552
+ }
553
+ }
554
+ }
555
+ }
556
+
557
+ if (allProjectedFilesExist) {
558
+ // Restore snapshotValues from cache
559
+ Object.assign(snapshotValues, merge(snapshotValues, cachedData.snapshotData))
560
+ timing?.record(`Projector: Cache HIT for ${capsule.cst.source.moduleFilepath}`)
561
+ return true
562
+ } else {
563
+ timing?.record(timing?.chalk?.yellow?.(`Projector: Cache INVALID (projected files missing) for ${capsule.cst.source.moduleFilepath}`))
564
+ }
565
+ }
566
+ }
567
+
568
+ // Recursively project all mapped capsules first
569
+ if (capsules) {
570
+ // Look through all capsules to find ones that are mapped by this capsule
571
+ for (const [key, potentialMappedCapsule] of Object.entries(capsules)) {
572
+ // Skip the current capsule
573
+ if (potentialMappedCapsule === capsule) continue
574
+
575
+ // Check if this capsule has the Capsule struct (meaning it should be projected)
576
+ if (potentialMappedCapsule.cst?.spineContracts?.[spineContractUri]?.properties?.['#@stream44.studio/encapsulate/structs/Capsule']) {
577
+ // Check if this capsule's moduleFilepath is referenced in any mapping property
578
+ const mappedModulePath = potentialMappedCapsule.cst.source.moduleFilepath
579
+
580
+ for (const [propContractKey, propContract] of Object.entries(spineContract.properties)) {
581
+ if (propContractKey.startsWith('#') && (propContract as any).properties) {
582
+ for (const [propName, propDef] of Object.entries((propContract as any).properties)) {
583
+ const prop = propDef as any
584
+ if (prop.type === 'CapsulePropertyTypes.Mapping') {
585
+ const mappingValue = prop.valueExpression?.replace(/['"]/g, '')
586
+ // Check if this mapping points to the potential mapped capsule
587
+ if (mappingValue && mappedModulePath && (
588
+ mappedModulePath.endsWith(mappingValue + '.ts') ||
589
+ mappedModulePath.endsWith(mappingValue + '.tsx') ||
590
+ mappedModulePath.includes(mappingValue.replace('./', ''))
591
+ )) {
592
+ await projectCapsule({ capsule: potentialMappedCapsule, capsules, snapshotValues, spineContractUri, projectingCapsules })
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
598
+ }
599
+ }
600
+ }
601
+ timing?.record(timing?.chalk?.red?.(`Projector: Cache MISS for ${capsule.cst.source.moduleFilepath}`))
602
+ } catch (error) {
603
+ // Cache miss or error, proceed with projection
604
+ }
605
+ }
606
+
607
+ const context = buildProjectionContext(capsule, spineContractUri, capsules)
608
+
609
+ // Set projectedCapsuleFilepath in snapshotValues using buildCapsuleSnapshotForReference
610
+ const snapshotData = await buildCapsuleSnapshotForReference({ value: capsule.cst }, capsules, spineContractUri)
611
+ Object.assign(snapshotValues, merge(snapshotValues, snapshotData))
612
+
613
+ // Retrieve the filepath from snapshotValues
614
+ const filepath = snapshotValues.spineContracts[spineContractUri]['#@stream44.studio/encapsulate/structs/Capsule'].projectedCapsuleFilepath
615
+
616
+ // Helper: Rewrite capsule expression to inline CST using regex
617
+ function rewriteCapsuleExpressionWithCST(targetCapsule: any): string {
618
+ let expression = targetCapsule.cst.source.capsuleExpression || ''
619
+
620
+ const cstJson = JSON.stringify(targetCapsule.cst, null, 4)
621
+ const crtJson = JSON.stringify(targetCapsule.crt || {}, null, 4)
622
+ const moduleFilepath = targetCapsule.cst.source.moduleFilepath
623
+ const importStackLine = targetCapsule.cst.source.importStackLine
624
+
625
+ // Replace importMeta: import.meta with moduleFilepath: '...'
626
+ expression = expression.replace(
627
+ /importMeta:\s*import\.meta/g,
628
+ `moduleFilepath: '${moduleFilepath}'`
629
+ )
630
+
631
+ // Replace importStack: makeImportStack() with importStackLine: ..., crt: {...}, cst: {...}
632
+ expression = expression.replace(
633
+ /importStack:\s*makeImportStack\(\)/g,
634
+ `importStackLine: ${importStackLine}, crt: ${crtJson}, cst: ${cstJson}`
635
+ )
636
+
637
+ // Remove ambientReferences from the expression since they're handled separately
638
+ // Match: ambientReferences: { ... } including multi-line with proper brace matching
639
+ expression = expression.replace(
640
+ /,?\s*ambientReferences:\s*\{[\s\S]*?\}/gm,
641
+ ''
642
+ )
643
+
644
+ return expression
645
+ }
646
+
647
+ let capsuleExpression = rewriteCapsuleExpressionWithCST(capsule)
648
+
649
+ let ambientReferences = capsule.cst.source.ambientReferences || {}
650
+
651
+ // Helper: Process CSS files for a capsule with mode-aware path resolution
652
+ async function processCssFiles(
653
+ targetCapsule: any,
654
+ mode: WriteMode,
655
+ targetPath: string | null,
656
+ sourceModuleFilepath: string
657
+ ): Promise<Record<string, string>> {
658
+ const cssImportMapping: Record<string, string> = {}
659
+ const capsuleAmbientRefs = targetCapsule.source?.ambientReferences || {}
660
+
661
+ for (const [name, ref] of Object.entries(capsuleAmbientRefs)) {
662
+ const refTyped = ref as any
663
+ if (refTyped.type === 'import' && refTyped.moduleUri.endsWith('.css')) {
664
+ const originalCssPath = refTyped.moduleUri
665
+ const cssBasename = basename(originalCssPath, '.css')
666
+
667
+ // Generate CSS paths based on mode
668
+ let cssFilePath: string
669
+ let cssImportPath: string
670
+
671
+ if (mode === 'temp') {
672
+ // Temp mode: use hash-based naming
673
+ cssFilePath = `./.~capsule-${targetCapsule.capsuleSourceNameRefHash.substring(0, 8)}-${cssBasename}.css`
674
+ cssImportPath = cssFilePath
675
+ } else {
676
+ // Projection mode: use target path-based naming
677
+ if (targetPath) {
678
+ const targetDir = dirname(targetPath)
679
+ const targetBase = basename(targetPath, '.tsx').replace(/\.ts$/, '')
680
+ cssFilePath = `${targetDir}/${targetBase}-${cssBasename}.css`
681
+ // Import path should be relative with ./ prefix
682
+ cssImportPath = `./${targetBase}-${cssBasename}.css`
683
+ } else {
684
+ // Fallback to temp naming
685
+ cssFilePath = `./.~capsule-${targetCapsule.capsuleSourceNameRefHash.substring(0, 8)}-${cssBasename}.css`
686
+ cssImportPath = cssFilePath
687
+ }
688
+ }
689
+
690
+ // Read source CSS file
691
+ try {
692
+ const sourceModuleDir = dirname(join(spineFilesystemRoot, sourceModuleFilepath))
693
+ const sourceCssPath = join(sourceModuleDir, originalCssPath)
694
+ const cssContent = await readFile(sourceCssPath, 'utf-8')
695
+
696
+ // Write CSS file based on mode
697
+ if (mode === 'temp') {
698
+ // Write relative to source module directory
699
+ const sourceDir = dirname(sourceModuleFilepath)
700
+ const relativeCssPath = join(sourceDir, cssFilePath)
701
+ await spineStore.writeFile(relativeCssPath, cssContent)
702
+ } else if (projectionStore) {
703
+ await projectionStore.writeFile(cssFilePath, cssContent)
704
+ }
705
+
706
+ cssImportMapping[originalCssPath] = cssImportPath
707
+ } catch (error) {
708
+ console.warn(`Warning: Could not copy CSS file ${originalCssPath}:`, error)
709
+ }
710
+ }
711
+ }
712
+
713
+ return cssImportMapping
714
+ }
715
+
716
+ // Process CSS files for temp mode
717
+ const cssImportMapping = await processCssFiles(
718
+ capsule.cst,
719
+ 'temp',
720
+ null,
721
+ capsule.cst.source.moduleFilepath
722
+ )
723
+
724
+ // Exclude makeImportStack from ambient references since it's being replaced with importStackLine
725
+ if (ambientReferences['makeImportStack']) {
726
+ ambientReferences = { ...ambientReferences }
727
+ delete ambientReferences['makeImportStack']
728
+ }
729
+
730
+ const importStatements = Object.entries(ambientReferences)
731
+ .map(([name, ref]: [string, any]) => {
732
+ if (ref.type === 'import') {
733
+ // Check if this is a CSS import (moduleUri ends with .css)
734
+ if (ref.moduleUri.endsWith('.css')) {
735
+ // Use the mapped CSS path if available
736
+ const cssPath = cssImportMapping[ref.moduleUri] || ref.moduleUri
737
+ return `import '${cssPath}'`
738
+ }
739
+ return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
740
+ }
741
+ if (ref.type === 'assigned') {
742
+ // If the assignment comes from a spine-factory module, import from encapsulate.ts instead
743
+ if (ref.moduleUri.includes('/spine-factories/')) {
744
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
745
+ }
746
+ }
747
+ if (ref.type === 'invocation-argument') {
748
+ // Only import if it's an available encapsulate export
749
+ if (ref.isEncapsulateExport) {
750
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
751
+ }
752
+ }
753
+ return ''
754
+ })
755
+ .filter(Boolean)
756
+ .join('\n')
757
+
758
+ const literalReferences = Object.entries(ambientReferences)
759
+ .map(([name, ref]: [string, any]) => {
760
+ if (ref.type === 'literal') {
761
+ return `const ${name} = ${JSON.stringify(ref.value)}`
762
+ }
763
+ return ''
764
+ })
765
+ .filter(Boolean)
766
+ .join('\n')
767
+
768
+ const instanceInitializationsPromises = Object.entries(ambientReferences)
769
+ .map(async ([name, ref]: [string, any]) => {
770
+ if (ref.type === 'capsule') {
771
+ const capsuleSnapshot = await buildCapsuleSnapshotForReference(ref, capsules, spineContractUri)
772
+ return `const ${name}_fn = await loadCapsule({ capsuleSnapshot: ${JSON.stringify(capsuleSnapshot, null, 4)} })\n const ${name} = await ${name}_fn({ encapsulate, loadCapsule })`
773
+ }
774
+ return ''
775
+ })
776
+ const instanceInitializations = (await Promise.all(instanceInitializationsPromises))
777
+ .filter(Boolean)
778
+ .join('\n ')
779
+
780
+ // Extract module-local code for functions marked as 'module-local'
781
+ const moduleLocalCode = capsule.cst.source.moduleLocalCode || {}
782
+ const moduleLocalFunctions = Object.entries(moduleLocalCode)
783
+ .map(([name, code]: [string, any]) => code)
784
+ .filter(Boolean)
785
+ .join('\n\n')
786
+
787
+ const allStatements = [importStatements, literalReferences, moduleLocalFunctions].filter(Boolean).join('\n')
788
+
789
+ // Check if this capsule has a solidjs.com or standalone property
790
+ const hasSolidJs = hasSolidJsProperty(capsule, spineContractUri)
791
+ const hasStandalone = hasStandaloneProperty(capsule, spineContractUri)
792
+ const needsRuntime = hasSolidJs || hasStandalone
793
+
794
+ // Determine which solid-js imports are needed (avoid duplicates with ambient references)
795
+ const existingSolidJsImports = new Set<string>()
796
+ for (const [name, ref] of Object.entries(ambientReferences)) {
797
+ const refTyped = ref as any
798
+ if (refTyped.type === 'import' && refTyped.moduleUri === 'solid-js') {
799
+ // Parse existing imports from solid-js
800
+ const match = refTyped.importSpecifier?.match(/\{([^}]+)\}/)
801
+ if (match) {
802
+ match[1].split(',').forEach((imp: string) => existingSolidJsImports.add(imp.trim()))
803
+ }
804
+ }
805
+ }
806
+
807
+ const neededSolidJsImports = ['createSignal', 'onMount', 'Show'].filter(imp => !existingSolidJsImports.has(imp))
808
+ const solidJsImport = hasSolidJs && neededSolidJsImports.length > 0 ? `import { ${neededSolidJsImports.join(', ')} } from 'solid-js'\n` : ''
809
+
810
+ // Add runtime imports for SolidJS and standalone functions
811
+ 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}` : ''
812
+
813
+ // Generate default export based on capsule type
814
+ let defaultExport = ''
815
+ if (hasSolidJs) {
816
+ // Generate a wrapper that sets up runtime and exports the SolidJS component
817
+ const capsuleSourceLineRef = capsule.cst.capsuleSourceLineRef
818
+ const solidjsComponent = extractSolidJsComponent(capsule, spineContractUri)
819
+ if (solidjsComponent) {
820
+ // Collect all capsule URIs from CST (mappings and property contracts)
821
+ const allCapsuleUris = collectAllCapsuleUris(capsule, spineContractUri)
822
+
823
+ // Also collect from ambient references and build import paths from snapshots
824
+ const capsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
825
+ for (const [name, ref] of Object.entries(ambientReferences)) {
826
+ const refTyped = ref as any
827
+ if (refTyped.type === 'capsule') {
828
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
829
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.capsuleName
830
+ const projectedFilepath = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.projectedCapsuleFilepath
831
+
832
+ if (capsuleName && projectedFilepath) {
833
+ allCapsuleUris.add(capsuleName)
834
+
835
+ // Build import path from projected filepath
836
+ const importName = `_capsule_${capsuleName.replace(/[^a-zA-Z0-9]/g, '_')}`
837
+ // Remove .~caps/ prefix and strip extension
838
+ const importPath = projectedFilepath.replace(/^\.~caps\//, '').replace(/\.(ts|tsx)$/, '')
839
+
840
+ capsuleDeps.push({ uri: capsuleName, importName, importPath })
841
+ }
842
+ }
843
+ }
844
+
845
+ // Generate static imports for all capsule dependencies
846
+ // Compute relative path from projected file to .~caps directory
847
+ let importPrefix: string
848
+ if (capsuleModuleProjectionPackage) {
849
+ importPrefix = capsuleModuleProjectionPackage
850
+ } else {
851
+ const projectedFileDir = dirname(filepath)
852
+ const capsDir = '.~caps'
853
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
854
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
855
+ }
856
+ const capsuleImports = capsuleDeps.map(dep =>
857
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
858
+ ).join('\n')
859
+
860
+ // Generate capsules map
861
+ const capsulesMapEntries = capsuleDeps.map(dep =>
862
+ ` '${dep.uri}': ${dep.importName}`
863
+ ).join(',\n')
864
+
865
+ defaultExport = `
866
+ ${capsuleImports}
867
+
868
+ // Set up runtime for browser execution
869
+ const sourceSpine: { encapsulate?: any } = {}
870
+
871
+ // Map of statically imported capsules
872
+ const capsulesMap: Record<string, any> = {
873
+ ${capsulesMapEntries}
874
+ }
875
+
876
+ // Helper to import and instantiate a capsule from the capsules map
877
+ const importCapsule = async (uri: string) => {
878
+ const capsuleModule = capsulesMap[uri]
879
+ if (!capsuleModule) {
880
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
881
+ }
882
+ const capsule = await capsuleModule.capsule({
883
+ encapsulate: sourceSpine.encapsulate,
884
+ loadCapsule
885
+ })
886
+ return capsule
887
+ }
888
+
889
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
890
+ // Return the capsule function from this projected file
891
+ if (capsuleSourceLineRef === '${capsuleSourceLineRef}') {
892
+ return capsule
893
+ }
894
+
895
+ // Use capsuleName directly if provided
896
+ if (capsuleName) {
897
+ return await importCapsule(capsuleName)
898
+ }
899
+
900
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
901
+ }
902
+
903
+ const spineContractOpts = {
904
+ spineFilesystemRoot: '.',
905
+ resolve: async (uri: string) => uri,
906
+ importCapsule
907
+ }
908
+
909
+ const runtimeSpineContracts = {
910
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
911
+ }
912
+
913
+ const snapshot = {
914
+ capsules: {
915
+ ['${capsuleSourceLineRef}']: {
916
+ spineContracts: {}
917
+ }
918
+ }
919
+ }
920
+
921
+ // Export wrapper function that initializes runtime and returns component
922
+ export default function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
923
+ const [component, setComponent] = createSignal(null)
924
+
925
+ onMount(async () => {
926
+ // Add onMembraneEvent to spine contract opts - use provided or default logger
927
+ const defaultMembraneLogger = (event: any) => {
928
+ console.log('[Membrane Event]', event)
929
+ }
930
+ const opts = {
931
+ ...spineContractOpts,
932
+ onMembraneEvent: onMembraneEvent || defaultMembraneLogger
933
+ }
934
+ const contracts = {
935
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(opts)
936
+ }
937
+
938
+ const { encapsulate, capsules } = await Spine({
939
+ spineFilesystemRoot: '.',
940
+ spineContracts: contracts
941
+ })
942
+
943
+ sourceSpine.encapsulate = encapsulate
944
+
945
+ const capsuleInstance = await capsule({ encapsulate, loadCapsule })
946
+
947
+ const { run } = await SpineRuntime({
948
+ spineFilesystemRoot: '.',
949
+ spineContracts: contracts,
950
+ snapshot,
951
+ loadCapsule
952
+ })
953
+
954
+ const Component = await run({}, async ({ apis }) => {
955
+ const capsuleApi = apis['${capsuleSourceLineRef}']
956
+ const solidjsKey = Object.keys(capsuleApi).find(k => k === 'solidjs.com/standalone')
957
+ if (!solidjsKey) throw new Error('solidjs.com/standalone property not found')
958
+ return capsuleApi[solidjsKey]()
959
+ })
960
+
961
+ setComponent(() => Component)
962
+ })
963
+
964
+ // Return the wrapper function itself, not call it
965
+ const WrapperComponent = () => {
966
+ const Component = component()
967
+ return Show({ when: Component, children: (Component) => Component() })
968
+ }
969
+
970
+ return WrapperComponent
971
+ }
972
+ `
973
+ }
974
+ } else if (hasStandalone) {
975
+ // Generate a wrapper function that directly invokes the standalone function
976
+ const capsuleSourceLineRef = capsule.cst.capsuleSourceLineRef
977
+
978
+ // Collect all capsule URIs from CST (mappings and property contracts)
979
+ const allCapsuleUris = collectAllCapsuleUris(capsule, spineContractUri)
980
+
981
+ // Also collect from ambient references
982
+ for (const [name, ref] of Object.entries(ambientReferences)) {
983
+ const refTyped = ref as any
984
+ if (refTyped.type === 'capsule') {
985
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
986
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.capsuleName
987
+ if (capsuleName) {
988
+ allCapsuleUris.add(capsuleName)
989
+ }
990
+ }
991
+ }
992
+
993
+ // Build import paths from snapshots for ALL collected URIs
994
+ const capsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
995
+ for (const uri of allCapsuleUris) {
996
+ // Find the capsule in the registry and build its snapshot
997
+ let capsuleRef = null
998
+ if (capsules) {
999
+ for (const [key, cap] of Object.entries(capsules)) {
1000
+ const capCapsuleName = cap.cst?.source?.capsuleName
1001
+ if (capCapsuleName === uri) {
1002
+ capsuleRef = { type: 'capsule', value: cap.cst }
1003
+ break
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ if (capsuleRef) {
1009
+ const snapshot = await buildCapsuleSnapshotForReference(capsuleRef, capsules, spineContractUri)
1010
+ const projectedFilepath = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.projectedCapsuleFilepath
1011
+
1012
+ if (projectedFilepath) {
1013
+ // Build import path from projected filepath
1014
+ const importName = `_capsule_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`
1015
+ // Remove .~caps/ prefix and strip extension
1016
+ const importPath = projectedFilepath.replace(/^\.~caps\//, '').replace(/\.(ts|tsx)$/, '')
1017
+
1018
+ capsuleDeps.push({ uri, importName, importPath })
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ // Generate static imports for all capsule dependencies
1024
+ // Compute relative path from projected file to .~caps directory
1025
+ let importPrefix: string
1026
+ if (capsuleModuleProjectionPackage) {
1027
+ importPrefix = capsuleModuleProjectionPackage
1028
+ } else {
1029
+ const projectedFileDir = dirname(filepath)
1030
+ const capsDir = '.~caps'
1031
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
1032
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
1033
+ }
1034
+ const capsuleImports = capsuleDeps.map(dep =>
1035
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
1036
+ ).join('\n')
1037
+
1038
+ // Generate capsules map
1039
+ const capsulesMapEntries = capsuleDeps.map(dep =>
1040
+ ` '${dep.uri}': ${dep.importName}`
1041
+ ).join(',\n')
1042
+
1043
+ defaultExport = `
1044
+ ${capsuleImports}
1045
+
1046
+ // Export standalone function - directly invoke the capsule API
1047
+ export default async function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
1048
+ // Use sourceSpine pattern like the factory to provide real encapsulate
1049
+ const sourceSpine: { encapsulate?: any } = {}
1050
+
1051
+ // Map of statically imported capsules
1052
+ const capsulesMap: Record<string, any> = {
1053
+ ${capsulesMapEntries}
1054
+ }
1055
+
1056
+ // Helper to import and instantiate a capsule from the capsules map
1057
+ const importCapsule = async (uri: string) => {
1058
+ const capsuleModule = capsulesMap[uri]
1059
+ if (!capsuleModule) {
1060
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
1061
+ }
1062
+ const capsule = await capsuleModule.capsule({
1063
+ encapsulate: sourceSpine.encapsulate,
1064
+ loadCapsule
1065
+ })
1066
+ return capsule
1067
+ }
1068
+
1069
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
1070
+ // Return the capsule function from this projected file
1071
+ if (capsuleSourceLineRef === '${capsuleSourceLineRef}') {
1072
+ return capsule
1073
+ }
1074
+
1075
+ // Use capsuleName directly if provided
1076
+ if (capsuleName) {
1077
+ return await importCapsule(capsuleName)
1078
+ }
1079
+
1080
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
1081
+ }
1082
+
1083
+ const spineContractOpts = {
1084
+ spineFilesystemRoot: '.',
1085
+ resolve: async (uri: string) => uri,
1086
+ importCapsule,
1087
+ ...(onMembraneEvent ? { onMembraneEvent } : {})
1088
+ }
1089
+
1090
+ const runtimeSpineContracts = {
1091
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
1092
+ }
1093
+
1094
+ const snapshot = {
1095
+ capsules: {
1096
+ ['${capsuleSourceLineRef}']: {
1097
+ spineContracts: {}
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ const { run, encapsulate } = await SpineRuntime({
1103
+ spineFilesystemRoot: '.',
1104
+ spineContracts: runtimeSpineContracts,
1105
+ snapshot,
1106
+ loadCapsule
1107
+ })
1108
+
1109
+ // Populate sourceSpine.encapsulate so imported capsules can use it
1110
+ sourceSpine.encapsulate = encapsulate
1111
+
1112
+ const result = await run({}, async ({ apis }) => {
1113
+ const capsuleApi = apis['${capsuleSourceLineRef}']
1114
+ const standaloneKey = Object.keys(capsuleApi).find(k => k === 'encapsulate.dev/standalone' || k.startsWith('encapsulate.dev/standalone/'))
1115
+ if (!standaloneKey) throw new Error('encapsulate.dev/standalone property not found')
1116
+ const standaloneFunc = capsuleApi[standaloneKey]()
1117
+ return standaloneFunc()
1118
+ })
1119
+
1120
+ return result
1121
+ }
1122
+ `
1123
+ }
1124
+
1125
+ // Prepare literal references with proper indentation
1126
+ const indentedLiteralRefs = literalReferences ? literalReferences.split('\n').map(line => line ? ` ${line}` : '').join('\n') + '\n' : ''
1127
+ const indentedInstanceInits = instanceInitializations ? ' ' + instanceInitializations.split('\n').join('\n ') + '\n' : ''
1128
+
1129
+ const fileContent = `${runtimeImport}${importStatements}
1130
+ ${moduleLocalFunctions}
1131
+
1132
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1133
+ ${indentedLiteralRefs}${indentedInstanceInits} return ${capsuleExpression}
1134
+ }
1135
+ capsule['#'] = ${JSON.stringify(capsule.cst.source.capsuleName || '')}
1136
+ ${defaultExport}
1137
+ `
1138
+ // Write to projection store
1139
+ if (projectionStore) {
1140
+ await projectionStore.writeFile(filepath, fileContent)
1141
+ }
1142
+
1143
+ // If this capsule has a custom projection path, also write there
1144
+ if (context.customProjectionPath) {
1145
+ // Process CSS files for projection mode
1146
+ const projectionCssMapping = await processCssFiles(
1147
+ capsule.cst,
1148
+ 'projection',
1149
+ context.customProjectionPath,
1150
+ capsule.cst.source.moduleFilepath
1151
+ )
1152
+
1153
+ // Update import statements to use projection CSS paths
1154
+ const projectionImportStatements = Object.entries(ambientReferences)
1155
+ .map(([name, ref]: [string, any]) => {
1156
+ if (ref.type === 'import') {
1157
+ if (ref.moduleUri.endsWith('.css')) {
1158
+ const cssPath = projectionCssMapping[ref.moduleUri] || ref.moduleUri
1159
+ return `import '${cssPath}'`
1160
+ }
1161
+ return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
1162
+ }
1163
+ if (ref.type === 'assigned') {
1164
+ if (ref.moduleUri.includes('/spine-factories/')) {
1165
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1166
+ }
1167
+ }
1168
+ if (ref.type === 'invocation-argument') {
1169
+ // Only import if it's an available encapsulate export
1170
+ if (ref.isEncapsulateExport) {
1171
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1172
+ }
1173
+ }
1174
+ return ''
1175
+ })
1176
+ .filter(Boolean)
1177
+ .join('\n')
1178
+
1179
+ const projectionAllStatements = [projectionImportStatements, literalReferences].filter(Boolean).join('\n')
1180
+ const projectionFileContent = `${runtimeImport}${projectionAllStatements}
1181
+
1182
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1183
+ ${instanceInitializations}
1184
+ return ${capsuleExpression}
1185
+ }
1186
+ capsule['#'] = ${JSON.stringify(capsule.cst.source.capsuleName || '')}
1187
+ ${defaultExport}
1188
+ `
1189
+ if (projectionStore) {
1190
+ await projectionStore.writeFile(context.customProjectionPath, projectionFileContent)
1191
+ }
1192
+ }
1193
+
1194
+ // Write mapped capsules to their projection paths
1195
+ for (const mapped of context.mappedCapsules) {
1196
+ // Get the full mapped capsule from the registry using its hash
1197
+ if (!capsules) {
1198
+ console.warn(`Warning: Cannot write mapped capsule - no capsules registry provided`)
1199
+ continue
1200
+ }
1201
+
1202
+ // Find the capsule in the registry by matching the hash
1203
+ let mappedCapsule = null
1204
+ for (const [key, cap] of Object.entries(capsules)) {
1205
+ if (cap.cst?.capsuleSourceNameRefHash === mapped.capsuleHash) {
1206
+ mappedCapsule = cap
1207
+ break
1208
+ }
1209
+ }
1210
+
1211
+ if (!mappedCapsule) {
1212
+ console.warn(`Warning: Mapped capsule with hash ${mapped.capsuleHash} not found in registry`)
1213
+ continue
1214
+ }
1215
+
1216
+ // Process CSS files for the mapped capsule in projection mode
1217
+ const mappedCssMapping = await processCssFiles(
1218
+ mappedCapsule.cst,
1219
+ 'projection',
1220
+ mapped.projectionPath,
1221
+ mappedCapsule.cst.source.moduleFilepath
1222
+ )
1223
+
1224
+ // Get ambient references for the mapped capsule
1225
+ const mappedAmbientRefs = mappedCapsule.cst.source?.ambientReferences || {}
1226
+
1227
+ // Generate import statements with projection CSS paths
1228
+ const mappedImportStatements = Object.entries(mappedAmbientRefs)
1229
+ .map(([name, ref]: [string, any]) => {
1230
+ if (ref.type === 'import') {
1231
+ if (ref.moduleUri.endsWith('.css')) {
1232
+ const cssPath = mappedCssMapping[ref.moduleUri] || ref.moduleUri
1233
+ return `import '${cssPath}'`
1234
+ }
1235
+ return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
1236
+ }
1237
+ if (ref.type === 'assigned') {
1238
+ if (ref.moduleUri.includes('/spine-factories/')) {
1239
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1240
+ }
1241
+ }
1242
+ if (ref.type === 'invocation-argument') {
1243
+ // Only import if it's an available encapsulate export
1244
+ if (ref.isEncapsulateExport) {
1245
+ return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
1246
+ }
1247
+ }
1248
+ return ''
1249
+ })
1250
+ .filter(Boolean)
1251
+ .join('\n')
1252
+
1253
+ const mappedLiteralReferences = Object.entries(mappedAmbientRefs)
1254
+ .map(([name, ref]: [string, any]) => {
1255
+ if (ref.type === 'literal') {
1256
+ return `const ${name} = ${JSON.stringify(ref.value)}`
1257
+ }
1258
+ return ''
1259
+ })
1260
+ .filter(Boolean)
1261
+ .join('\n')
1262
+
1263
+ const mappedInstanceInitializationsPromises = Object.entries(mappedAmbientRefs)
1264
+ .map(async ([name, ref]: [string, any]) => {
1265
+ if (ref.type === 'capsule') {
1266
+ const capsuleSnapshot = await buildCapsuleSnapshotForReference(ref, capsules, spineContractUri)
1267
+ return `const ${name}_fn = await loadCapsule({ capsuleSnapshot: ${JSON.stringify(capsuleSnapshot, null, 4)} })\n const ${name} = await ${name}_fn({ encapsulate, loadCapsule })`
1268
+ }
1269
+ return ''
1270
+ })
1271
+ const mappedInstanceInitializations = (await Promise.all(mappedInstanceInitializationsPromises))
1272
+ .filter(Boolean)
1273
+ .join('\n ')
1274
+
1275
+ // Extract module-local code for mapped capsule
1276
+ const mappedModuleLocalCode = mappedCapsule.cst.source?.moduleLocalCode || {}
1277
+ const mappedModuleLocalFunctions = Object.entries(mappedModuleLocalCode)
1278
+ .map(([name, code]: [string, any]) => code)
1279
+ .filter(Boolean)
1280
+ .join('\n\n')
1281
+
1282
+ const mappedAllStatements = [mappedImportStatements, mappedLiteralReferences, mappedModuleLocalFunctions].filter(Boolean).join('\n')
1283
+
1284
+ // Check if mapped capsule has solidjs.com or encapsulate.dev/standalone property
1285
+ const mappedHasSolidJs = hasSolidJsProperty(mappedCapsule, spineContractUri)
1286
+ const mappedHasStandalone = hasStandaloneProperty(mappedCapsule, spineContractUri)
1287
+ const mappedNeedsRuntime = mappedHasSolidJs || mappedHasStandalone
1288
+
1289
+ // Determine which solid-js imports are needed for mapped capsule
1290
+ const mappedExistingSolidJsImports = new Set<string>()
1291
+ for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
1292
+ const refTyped = ref as any
1293
+ if (refTyped.type === 'import' && refTyped.moduleUri === 'solid-js') {
1294
+ const match = refTyped.importSpecifier?.match(/\{([^}]+)\}/)
1295
+ if (match) {
1296
+ match[1].split(',').forEach((imp: string) => mappedExistingSolidJsImports.add(imp.trim()))
1297
+ }
1298
+ }
1299
+ }
1300
+
1301
+ const mappedNeededSolidJsImports = ['createSignal', 'onMount', 'Show'].filter(imp => !mappedExistingSolidJsImports.has(imp))
1302
+ const mappedSolidJsImport = mappedHasSolidJs && mappedNeededSolidJsImports.length > 0 ? `import { ${mappedNeededSolidJsImports.join(', ')} } from 'solid-js'\n` : ''
1303
+
1304
+ // Rewrite the mapped capsule expression to include CST (reuse the same function)
1305
+ const mappedCapsuleExpression = rewriteCapsuleExpressionWithCST(mappedCapsule)
1306
+
1307
+ // Add runtime imports for SolidJS and standalone functions
1308
+ 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}` : ''
1309
+
1310
+ let mappedDefaultExport = ''
1311
+ if (mappedHasSolidJs) {
1312
+ // Generate a wrapper that sets up runtime and exports the SolidJS component
1313
+ const mappedCapsuleSourceLineRef = mappedCapsule.cst.capsuleSourceLineRef
1314
+ const solidjsComponent = extractSolidJsComponent(mappedCapsule, spineContractUri)
1315
+ if (solidjsComponent) {
1316
+ // Collect all capsule URIs from CST (mappings and property contracts)
1317
+ const allMappedCapsuleUris = collectAllCapsuleUris(mappedCapsule, spineContractUri)
1318
+
1319
+ // Also collect from ambient references
1320
+ for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
1321
+ const refTyped = ref as any
1322
+ if (refTyped.type === 'capsule') {
1323
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
1324
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.capsuleName
1325
+ if (capsuleName) {
1326
+ allMappedCapsuleUris.add(capsuleName)
1327
+ }
1328
+ }
1329
+ }
1330
+
1331
+ // Build capsule dependencies array
1332
+ const mappedCapsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
1333
+ for (const uri of allMappedCapsuleUris) {
1334
+ const importName = `_capsule_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`
1335
+ // Strip leading @ from URI to avoid double @ in import paths
1336
+ const importPath = uri.startsWith('@') ? uri.substring(1) : uri
1337
+ mappedCapsuleDeps.push({ uri, importName, importPath })
1338
+ }
1339
+
1340
+ // Generate static imports for all capsule dependencies
1341
+ // Compute relative path from projected file to .~caps directory
1342
+ let importPrefix: string
1343
+ if (capsuleModuleProjectionPackage) {
1344
+ importPrefix = capsuleModuleProjectionPackage
1345
+ } else {
1346
+ const projectedFileDir = dirname(mapped.projectionPath)
1347
+ const capsDir = '.~caps'
1348
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
1349
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
1350
+ }
1351
+ const mappedCapsuleImports = mappedCapsuleDeps.map(dep =>
1352
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
1353
+ ).join('\n')
1354
+
1355
+ // Generate capsules map
1356
+ const mappedCapsulesMapEntries = mappedCapsuleDeps.map(dep =>
1357
+ ` '${dep.uri}': ${dep.importName}`
1358
+ ).join(',\n')
1359
+
1360
+ mappedDefaultExport = `
1361
+ ${mappedCapsuleImports}
1362
+
1363
+ // Set up runtime for browser execution
1364
+ const sourceSpine: { encapsulate?: any } = {}
1365
+
1366
+ // Map of statically imported capsules
1367
+ const capsulesMap: Record<string, any> = {
1368
+ ${mappedCapsulesMapEntries}
1369
+ }
1370
+
1371
+ // Helper to import and instantiate a capsule from the capsules map
1372
+ const importCapsule = async (uri: string) => {
1373
+ const capsuleModule = capsulesMap[uri]
1374
+ if (!capsuleModule) {
1375
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
1376
+ }
1377
+ const capsule = await capsuleModule.capsule({
1378
+ encapsulate: sourceSpine.encapsulate,
1379
+ loadCapsule
1380
+ })
1381
+ return capsule
1382
+ }
1383
+
1384
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
1385
+ // Return the capsule function from this projected file
1386
+ if (capsuleSourceLineRef === '${mappedCapsuleSourceLineRef}') {
1387
+ return capsule
1388
+ }
1389
+
1390
+ // Use capsuleName directly if provided
1391
+ if (capsuleName) {
1392
+ return await importCapsule(capsuleName)
1393
+ }
1394
+
1395
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
1396
+ }
1397
+
1398
+ const spineContractOpts = {
1399
+ spineFilesystemRoot: '.',
1400
+ resolve: async (uri: string) => uri,
1401
+ importCapsule
1402
+ }
1403
+
1404
+ const runtimeSpineContracts = {
1405
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
1406
+ }
1407
+
1408
+ const snapshot = {
1409
+ capsules: {
1410
+ ['${mappedCapsuleSourceLineRef}']: {
1411
+ spineContracts: {}
1412
+ }
1413
+ }
1414
+ }
1415
+
1416
+ // Export wrapper function that initializes runtime and returns component
1417
+ export default function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
1418
+ const [component, setComponent] = createSignal(null)
1419
+
1420
+ onMount(async () => {
1421
+ // Add onMembraneEvent to spine contract opts - use provided or default logger
1422
+ const defaultMembraneLogger = (event: any) => {
1423
+ console.log('[Membrane Event]', event.type, event)
1424
+ }
1425
+ const opts = {
1426
+ ...spineContractOpts,
1427
+ onMembraneEvent: onMembraneEvent || defaultMembraneLogger
1428
+ }
1429
+ const contracts = {
1430
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(opts)
1431
+ }
1432
+
1433
+ const { encapsulate, capsules } = await Spine({
1434
+ spineFilesystemRoot: '.',
1435
+ spineContracts: contracts
1436
+ })
1437
+
1438
+ sourceSpine.encapsulate = encapsulate
1439
+
1440
+ const capsuleInstance = await capsule({ encapsulate, loadCapsule })
1441
+
1442
+ const { run } = await SpineRuntime({
1443
+ spineFilesystemRoot: '.',
1444
+ spineContracts: contracts,
1445
+ snapshot,
1446
+ loadCapsule
1447
+ })
1448
+
1449
+ const Component = await run({}, async ({ apis }) => {
1450
+ const capsuleApi = apis['${mappedCapsuleSourceLineRef}']
1451
+ const solidjsKey = Object.keys(capsuleApi).find(k => k === 'solidjs.com/standalone')
1452
+ if (!solidjsKey) throw new Error('solidjs.com/standalone property not found')
1453
+ return capsuleApi[solidjsKey]()
1454
+ })
1455
+
1456
+ setComponent(() => Component)
1457
+ })
1458
+
1459
+ return () => {
1460
+ const Component = component()
1461
+ return Show({ when: Component, children: (Component) => Component() })
1462
+ }
1463
+ }
1464
+ `
1465
+ }
1466
+ } else if (mappedHasStandalone) {
1467
+ // Generate a wrapper function that directly invokes the standalone function
1468
+ const mappedCapsuleSourceLineRef = mappedCapsule.cst.capsuleSourceLineRef
1469
+
1470
+ // Collect all capsule URIs from CST (mappings and property contracts)
1471
+ const allMappedCapsuleUris = collectAllCapsuleUris(mappedCapsule, spineContractUri)
1472
+
1473
+ // Also collect from ambient references
1474
+ for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
1475
+ const refTyped = ref as any
1476
+ if (refTyped.type === 'capsule') {
1477
+ const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
1478
+ const capsuleName = snapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.capsuleName
1479
+ if (capsuleName) {
1480
+ allMappedCapsuleUris.add(capsuleName)
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ // Build capsule dependencies array
1486
+ const mappedCapsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
1487
+ for (const uri of allMappedCapsuleUris) {
1488
+ const importName = `_capsule_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`
1489
+ // Strip leading @ from URI to avoid double @ in import paths
1490
+ const importPath = uri.startsWith('@') ? uri.substring(1) : uri
1491
+ mappedCapsuleDeps.push({ uri, importName, importPath })
1492
+ }
1493
+
1494
+ // Generate static imports for all capsule dependencies
1495
+ // Compute relative path from projected file to .~caps directory
1496
+ let importPrefix: string
1497
+ if (capsuleModuleProjectionPackage) {
1498
+ importPrefix = capsuleModuleProjectionPackage
1499
+ } else {
1500
+ const projectedFileDir = dirname(mapped.projectionPath)
1501
+ const capsDir = '.~caps'
1502
+ const relativePathToCaps = relative(projectedFileDir, capsDir)
1503
+ importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
1504
+ }
1505
+ const mappedCapsuleImports = mappedCapsuleDeps.map(dep =>
1506
+ `import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
1507
+ ).join('\n')
1508
+
1509
+ // Generate capsules map
1510
+ const mappedCapsulesMapEntries = mappedCapsuleDeps.map(dep =>
1511
+ ` '${dep.uri}': ${dep.importName}`
1512
+ ).join(',\n')
1513
+
1514
+ mappedDefaultExport = `
1515
+ ${mappedCapsuleImports}
1516
+
1517
+ // Export standalone function - directly invoke the capsule API
1518
+ export default async function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
1519
+ // Use sourceSpine pattern like the factory to provide real encapsulate
1520
+ const sourceSpine: { encapsulate?: any } = {}
1521
+
1522
+ // Map of statically imported capsules
1523
+ const capsulesMap: Record<string, any> = {
1524
+ ${mappedCapsulesMapEntries}
1525
+ }
1526
+
1527
+ // Helper to import and instantiate a capsule from the capsules map
1528
+ const importCapsule = async (uri: string) => {
1529
+ const capsuleModule = capsulesMap[uri]
1530
+ if (!capsuleModule) {
1531
+ throw new Error(\`Capsule not found in static imports: \${uri}\`)
1532
+ }
1533
+ const capsule = await capsuleModule.capsule({
1534
+ encapsulate: sourceSpine.encapsulate,
1535
+ loadCapsule
1536
+ })
1537
+ return capsule
1538
+ }
1539
+
1540
+ const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
1541
+ // Return the capsule function from this projected file
1542
+ if (capsuleSourceLineRef === '${mappedCapsuleSourceLineRef}') {
1543
+ return capsule
1544
+ }
1545
+
1546
+ // Use capsuleName directly if provided
1547
+ if (capsuleName) {
1548
+ return await importCapsule(capsuleName)
1549
+ }
1550
+
1551
+ throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
1552
+ }
1553
+
1554
+ const spineContractOpts = {
1555
+ spineFilesystemRoot: '.',
1556
+ resolve: async (uri: string) => uri,
1557
+ importCapsule,
1558
+ ...(onMembraneEvent ? { onMembraneEvent } : {})
1559
+ }
1560
+
1561
+ const runtimeSpineContracts = {
1562
+ ['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
1563
+ }
1564
+
1565
+ const snapshot = {
1566
+ capsules: {
1567
+ ['${mappedCapsuleSourceLineRef}']: {
1568
+ spineContracts: {}
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ const { run, encapsulate } = await SpineRuntime({
1574
+ spineFilesystemRoot: '.',
1575
+ spineContracts: runtimeSpineContracts,
1576
+ snapshot,
1577
+ loadCapsule
1578
+ })
1579
+
1580
+ // Populate sourceSpine.encapsulate so imported capsules can use it
1581
+ sourceSpine.encapsulate = encapsulate
1582
+
1583
+ const result = await run({}, async ({ apis }) => {
1584
+ const capsuleApi = apis['${mappedCapsuleSourceLineRef}']
1585
+ const standaloneKey = Object.keys(capsuleApi).find(k => k === 'encapsulate.dev/standalone' || k.startsWith('encapsulate.dev/standalone/'))
1586
+ if (!standaloneKey) throw new Error('encapsulate.dev/standalone property not found')
1587
+ const standaloneFunc = capsuleApi[standaloneKey]()
1588
+ return standaloneFunc()
1589
+ })
1590
+
1591
+ return result
1592
+ }
1593
+ `
1594
+ }
1595
+
1596
+ const mappedFileContent = `${mappedRuntimeImport}${mappedAllStatements}
1597
+
1598
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1599
+ ${mappedInstanceInitializations}
1600
+ return ${mappedCapsuleExpression}
1601
+ }
1602
+ capsule['#'] = ${JSON.stringify(mappedCapsule.cst.source.capsuleName || '')}
1603
+ ${mappedDefaultExport}
1604
+ `
1605
+
1606
+ // Write mapped capsule to projection path
1607
+ if (projectionStore) {
1608
+ await projectionStore.writeFile(mapped.projectionPath, mappedFileContent)
1609
+ }
1610
+ }
1611
+
1612
+ // Write projection cache metadata for main capsule
1613
+ if (projectionCacheStore?.writeFile) {
1614
+ try {
1615
+ const cacheFilePath = await constructCacheFilePath(
1616
+ capsule.cst.source.moduleFilepath,
1617
+ capsule.cst.source.importStackLine,
1618
+ spineFilesystemRoot
1619
+ )
1620
+ const cacheFilename = `${cacheFilePath}.projection.json`
1621
+ const cacheData = {
1622
+ snapshotData: await buildCapsuleSnapshotForReference({ value: capsule.cst }, capsules, spineContractUri)
1623
+ }
1624
+ await projectionCacheStore.writeFile(cacheFilename, JSON.stringify(cacheData, null, 2))
1625
+ } catch (error) {
1626
+ // Cache write error, continue without failing
1627
+ console.warn(`Warning: Failed to write projection cache for ${capsule.cst.source.moduleFilepath}:`, error)
1628
+ }
1629
+ }
1630
+
1631
+ // Write projection cache AND project ALL capsules in the registry to .~caps
1632
+ // This includes struct definitions, property contract capsules, and any other capsules
1633
+ if (projectionCacheStore?.writeFile && capsules) {
1634
+ for (const [capsuleKey, registryCapsule] of Object.entries(capsules)) {
1635
+ // Skip if this is the main capsule we already processed
1636
+ if (registryCapsule === capsule) {
1637
+ continue
1638
+ }
1639
+
1640
+ // Skip if this capsule doesn't have the required CST structure
1641
+ if (!registryCapsule.cst?.source?.moduleFilepath || !registryCapsule.cst?.source?.importStackLine) {
1642
+ continue
1643
+ }
1644
+
1645
+ try {
1646
+ // Write projection cache
1647
+ const capsuleCacheFilePath = await constructCacheFilePath(
1648
+ registryCapsule.cst.source.moduleFilepath,
1649
+ registryCapsule.cst.source.importStackLine,
1650
+ spineFilesystemRoot
1651
+ )
1652
+ const capsuleCacheFilename = `${capsuleCacheFilePath}.projection.json`
1653
+ const capsuleCacheData = {
1654
+ snapshotData: await buildCapsuleSnapshotForReference({ value: registryCapsule.cst }, capsules, spineContractUri)
1655
+ }
1656
+ await projectionCacheStore.writeFile(capsuleCacheFilename, JSON.stringify(capsuleCacheData, null, 2))
1657
+
1658
+ // Also project the capsule to .~caps
1659
+ const projectedPath = capsuleCacheData.snapshotData.spineContracts[spineContractUri]['#@stream44.studio/encapsulate/structs/Capsule'].projectedCapsuleFilepath
1660
+
1661
+ // Generate the capsule file content with proper imports and ambient reference loading
1662
+ const capsuleExpression = rewriteCapsuleExpressionWithCST(registryCapsule)
1663
+
1664
+ // Get ambient references for this capsule
1665
+ const capsuleAmbientRefs = registryCapsule.cst.source?.ambientReferences || {}
1666
+
1667
+ // Generate literal references
1668
+ const capsuleLiteralRefs = Object.entries(capsuleAmbientRefs)
1669
+ .map(([name, ref]: [string, any]) => {
1670
+ if (ref.type === 'literal') {
1671
+ return `const ${name} = ${JSON.stringify(ref.value)}`
1672
+ }
1673
+ return ''
1674
+ })
1675
+ .filter(Boolean)
1676
+ .join('\n')
1677
+
1678
+ // Generate instance initializations for ambient references
1679
+ const capsuleInstanceInitPromises = Object.entries(capsuleAmbientRefs)
1680
+ .map(async ([name, ref]: [string, any]) => {
1681
+ if (ref.type === 'capsule') {
1682
+ const capsuleSnapshot = await buildCapsuleSnapshotForReference(ref, capsules, spineContractUri)
1683
+ return `const ${name}_fn = await loadCapsule({ capsuleSnapshot: ${JSON.stringify(capsuleSnapshot, null, 4)} })\n const ${name} = await ${name}_fn({ encapsulate, loadCapsule })`
1684
+ }
1685
+ return ''
1686
+ })
1687
+ const capsuleInstanceInits = (await Promise.all(capsuleInstanceInitPromises))
1688
+ .filter(Boolean)
1689
+ .join('\n ')
1690
+
1691
+ // Add necessary imports
1692
+ const imports = `import { CapsulePropertyTypes } from '@stream44.studio/encapsulate/encapsulate'
1693
+ import { makeImportStack } from '@stream44.studio/encapsulate/encapsulate'
1694
+ `
1695
+
1696
+ // Get the capsule name for the assignment
1697
+ const capsuleName = registryCapsule.cst.source.capsuleName || ''
1698
+
1699
+ // Combine literal refs and instance inits with proper indentation
1700
+ const indentedLiterals = capsuleLiteralRefs ? capsuleLiteralRefs.split('\n').map(line => ` ${line}`).join('\n') + '\n' : ''
1701
+ const indentedInits = capsuleInstanceInits ? ' ' + capsuleInstanceInits + '\n' : ''
1702
+
1703
+ const capsuleFileContent = `${imports}
1704
+ export async function capsule({ encapsulate, loadCapsule }: { encapsulate: any, loadCapsule: any }) {
1705
+ ${indentedLiterals}${indentedInits} return ${capsuleExpression}
1706
+ }
1707
+ capsule['#'] = ${JSON.stringify(capsuleName)}
1708
+ `
1709
+
1710
+ if (projectionStore) {
1711
+ await projectionStore.writeFile(projectedPath, capsuleFileContent)
1712
+ }
1713
+ } catch (error) {
1714
+ console.warn(`Warning: Failed to write projection cache for capsule ${registryCapsule.cst.source.moduleFilepath}:`, error)
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ return true
1720
+ }
1721
+
1722
+ return {
1723
+ projectCapsule
1724
+ }
1725
+ }