@stream44.studio/encapsulate 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/package.json +21 -0
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +1716 -0
- package/src/encapsulate.ts +662 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +624 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +28 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +290 -0
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +299 -0
- package/src/spine-factories/TimingObserver.ts +26 -0
- package/src/static-analyzer.v0.ts +1591 -0
- package/structs/Capsule.v0.ts +22 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,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
|
+
}
|