@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,662 @@
|
|
|
1
|
+
|
|
2
|
+
type TSpineOptions = {
|
|
3
|
+
spineFilesystemRoot?: string,
|
|
4
|
+
spineContracts: Record<string, any>,
|
|
5
|
+
staticAnalyzer?: any,
|
|
6
|
+
timing?: { record: (step: string) => void, chalk?: any }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type TSpineRunOptions = {
|
|
10
|
+
overrides?: Record<string, any>,
|
|
11
|
+
options?: Record<string, any>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type TSpineRootsInvocationHandler = (context: { apis: Record<string, any>, capsules?: Record<string, any> }) => Promise<any>
|
|
15
|
+
|
|
16
|
+
type TSpine = {
|
|
17
|
+
freeze: () => Promise<TSpineSnapshot>,
|
|
18
|
+
encapsulate: (definition: TCapsuleDefinition, options: TCapsuleOptions) => Promise<TCapsule>,
|
|
19
|
+
capsules: Record<string, any>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type TSpineSnapshot = {
|
|
23
|
+
capsules: Record<string, Record<string, any>>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type TSpineRuntime = {
|
|
27
|
+
run: (options: TSpineRunOptions, handler: TSpineRootsInvocationHandler) => Promise<any>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type TSpineRuntimeOptions = {
|
|
31
|
+
spineFilesystemRoot?: string,
|
|
32
|
+
spineContracts?: Record<string, any>,
|
|
33
|
+
snapshot?: TSpineSnapshot,
|
|
34
|
+
capsules?: Record<string, any>,
|
|
35
|
+
loadCapsule?: (options: { capsuleSourceLineRef: string, capsuleSnapshot: any, capsuleName?: string }) => Promise<any>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type TCapsuleSnapshot = {
|
|
39
|
+
cst: any,
|
|
40
|
+
spineContracts: Record<string, any>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type TCapsuleMakeInstanceOptions = {
|
|
44
|
+
overrides?: Record<string, any>,
|
|
45
|
+
options?: Record<string, any>,
|
|
46
|
+
runtimeSpineContracts?: Record<string, any>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type TCapsule = {
|
|
50
|
+
capsuleSourceLineRef: string,
|
|
51
|
+
definition: TCapsuleDefinition,
|
|
52
|
+
encapsulateOptions: TEncapsulateOptions,
|
|
53
|
+
cst?: any,
|
|
54
|
+
crt?: any,
|
|
55
|
+
makeInstance: (options?: TCapsuleMakeInstanceOptions) => any,
|
|
56
|
+
toCapsuleReference: () => { capsuleSourceLineRef: string, capsuleSourceNameRefHash: any }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Spine contract URI -> Property contract name -> Property definitions
|
|
60
|
+
// Property contracts can be empty {} (for struct markers) or contain property definitions
|
|
61
|
+
type TCapsuleDefinition = Record<string, Record<string, {} | Record<string, { type: keyof typeof CapsulePropertyTypes, [key: string]: any }>>>
|
|
62
|
+
|
|
63
|
+
type TCapsuleOptions = {
|
|
64
|
+
importMeta?: {
|
|
65
|
+
url: string
|
|
66
|
+
},
|
|
67
|
+
importStack?: string,
|
|
68
|
+
importStackLine?: number,
|
|
69
|
+
moduleFilepath?: string,
|
|
70
|
+
capsuleName?: string,
|
|
71
|
+
ambientReferences?: Record<string, any>,
|
|
72
|
+
cst?: any,
|
|
73
|
+
crt?: any
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type TEncapsulateOptions = {
|
|
77
|
+
moduleFilepath: string,
|
|
78
|
+
importStackLine: number,
|
|
79
|
+
capsuleName?: string,
|
|
80
|
+
ambientReferences?: Record<string, any>,
|
|
81
|
+
capsuleSourceLineRef: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type TSpineContext = {
|
|
85
|
+
spineOptions: TSpineOptions,
|
|
86
|
+
spineContracts: Record<string, any>,
|
|
87
|
+
capsules: Record<string, any>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
export const CapsulePropertyTypes = {
|
|
92
|
+
Function: 'Function' as const,
|
|
93
|
+
GetterFunction: 'GetterFunction' as const,
|
|
94
|
+
String: 'String' as const,
|
|
95
|
+
Mapping: 'Mapping' as const,
|
|
96
|
+
Literal: 'Literal' as const,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ##################################################
|
|
100
|
+
// # Spine
|
|
101
|
+
// ##################################################
|
|
102
|
+
|
|
103
|
+
export async function SpineRuntime(options: TSpineRuntimeOptions): Promise<TSpineRuntime> {
|
|
104
|
+
|
|
105
|
+
const spineContracts = options.spineContracts || {}
|
|
106
|
+
const capsules: Record<string, any> = {}
|
|
107
|
+
|
|
108
|
+
const loadedCapsules: Record<string, any> = options.capsules || {}
|
|
109
|
+
|
|
110
|
+
const spine = {
|
|
111
|
+
run: async function (
|
|
112
|
+
runOptions: TSpineRunOptions,
|
|
113
|
+
handler: TSpineRootsInvocationHandler
|
|
114
|
+
): Promise<any> {
|
|
115
|
+
|
|
116
|
+
const capsules: Record<string, any> = {}
|
|
117
|
+
|
|
118
|
+
const hydratedSnapshots: Record<string, any> = {}
|
|
119
|
+
|
|
120
|
+
// Ensure all capsules are hydrated.
|
|
121
|
+
await Promise.all(Object.entries(loadedCapsules).map(async ([capsuleSourceLineRef, capsule]) => {
|
|
122
|
+
|
|
123
|
+
const hydratedSnapshot = options.snapshot?.capsules?.[capsuleSourceLineRef]
|
|
124
|
+
if (!hydratedSnapshot) return
|
|
125
|
+
|
|
126
|
+
await Promise.all(Object.entries(hydratedSnapshot.spineContracts).map(async ([spineContractUri, capsuleContractSnapshot]) => {
|
|
127
|
+
hydratedSnapshot.spineContracts[spineContractUri] = spineContracts[spineContractUri].hydrate({
|
|
128
|
+
capsuleSnapshot: capsuleContractSnapshot
|
|
129
|
+
})
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
hydratedSnapshots[capsuleSourceLineRef] = hydratedSnapshot
|
|
133
|
+
if (capsule.encapsulateOptions.capsuleName) {
|
|
134
|
+
hydratedSnapshots[capsule.encapsulateOptions.capsuleName] = hydratedSnapshot
|
|
135
|
+
}
|
|
136
|
+
}))
|
|
137
|
+
|
|
138
|
+
// Extract only the spine contract properties from hydrated snapshots
|
|
139
|
+
const hydratedOverrides: Record<string, any> = {}
|
|
140
|
+
for (const [capsuleRef, snapshot] of Object.entries(hydratedSnapshots)) {
|
|
141
|
+
if (snapshot.spineContracts) {
|
|
142
|
+
// Merge all spine contract properties into a single object for this capsule
|
|
143
|
+
const capsuleOverrides: Record<string, any> = {}
|
|
144
|
+
for (const [spineContractUri, spineContractData] of Object.entries(snapshot.spineContracts)) {
|
|
145
|
+
Object.assign(capsuleOverrides, spineContractData)
|
|
146
|
+
}
|
|
147
|
+
hydratedOverrides[capsuleRef] = capsuleOverrides
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const overrides = merge(
|
|
152
|
+
hydratedOverrides,
|
|
153
|
+
runOptions.overrides || {}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Helper function to create a proxy that dynamically unwraps .api layers
|
|
157
|
+
function createUnwrappingProxy(obj: any): any {
|
|
158
|
+
if (!obj || typeof obj !== 'object') return obj
|
|
159
|
+
|
|
160
|
+
// If this object has an .api property, create a proxy for it
|
|
161
|
+
if (obj.api && typeof obj.api === 'object') {
|
|
162
|
+
return new Proxy(obj.api, {
|
|
163
|
+
get: (target: any, prop: string | symbol) => {
|
|
164
|
+
if (typeof prop === 'symbol') return target[prop]
|
|
165
|
+
|
|
166
|
+
let value = target[prop]
|
|
167
|
+
|
|
168
|
+
// If the value is a Proxy (from Membrane), get the actual value through it
|
|
169
|
+
// The Proxy will return the correct value from its get trap
|
|
170
|
+
if (value && typeof value === 'object') {
|
|
171
|
+
// Check if this value has .api - if so, unwrap it
|
|
172
|
+
if (value.api && typeof value.api === 'object') {
|
|
173
|
+
return createUnwrappingProxy(value)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return value
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return obj
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const apis: Record<string, any> = {}
|
|
186
|
+
|
|
187
|
+
// Group keys by capsule object to avoid duplicate processing
|
|
188
|
+
const capsuleToKeys = new Map<any, string[]>()
|
|
189
|
+
for (const [key, capsule] of Object.entries(loadedCapsules)) {
|
|
190
|
+
if (!capsuleToKeys.has(capsule)) {
|
|
191
|
+
capsuleToKeys.set(capsule, [])
|
|
192
|
+
}
|
|
193
|
+
capsuleToKeys.get(capsule)!.push(key)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Instantiate each unique capsule once
|
|
197
|
+
for (const [capsule, keys] of capsuleToKeys) {
|
|
198
|
+
const instance = await capsule.makeInstance({
|
|
199
|
+
overrides,
|
|
200
|
+
options: runOptions.options?.[keys[0]],
|
|
201
|
+
runtimeSpineContracts: spineContracts
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Register instance under all keys that reference this capsule
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
capsules[key] = {
|
|
207
|
+
capsule,
|
|
208
|
+
instance,
|
|
209
|
+
makeInstance: undefined
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Create proxy that dynamically unwraps .api layers
|
|
213
|
+
apis[key] = createUnwrappingProxy(instance)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = await handler({ apis, capsules })
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
encapsulate: async function (definition: TCapsuleDefinition, encapsulateOptions: TCapsuleOptions): Promise<TCapsule> {
|
|
223
|
+
|
|
224
|
+
return encapsulate(definition, encapsulateOptions, {
|
|
225
|
+
spineOptions: {
|
|
226
|
+
spineFilesystemRoot: options.spineFilesystemRoot,
|
|
227
|
+
spineContracts,
|
|
228
|
+
staticAnalyzer: (options as any).staticAnalyzer
|
|
229
|
+
},
|
|
230
|
+
spineContracts,
|
|
231
|
+
capsules
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options.snapshot) {
|
|
237
|
+
|
|
238
|
+
await Promise.all(Object.entries(options.snapshot.capsules).map(async ([capsuleSourceLineRef, capsuleSnapshot]) => {
|
|
239
|
+
|
|
240
|
+
if (typeof loadedCapsules[capsuleSourceLineRef] !== 'undefined') return
|
|
241
|
+
|
|
242
|
+
// Extract capsuleName from snapshot if available
|
|
243
|
+
const capsuleName = capsuleSnapshot?.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.capsuleName
|
|
244
|
+
|
|
245
|
+
const capsule = await options.loadCapsule!({
|
|
246
|
+
capsuleSourceLineRef,
|
|
247
|
+
capsuleSnapshot,
|
|
248
|
+
capsuleName
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
loadedCapsules[capsuleSourceLineRef] = await capsule({
|
|
252
|
+
encapsulate: spine.encapsulate,
|
|
253
|
+
CapsulePropertyTypes,
|
|
254
|
+
makeImportStack,
|
|
255
|
+
loadCapsule: options.loadCapsule
|
|
256
|
+
})
|
|
257
|
+
}))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return spine
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function Spine(options: TSpineOptions): Promise<TSpine> {
|
|
264
|
+
|
|
265
|
+
const spineOptions = options
|
|
266
|
+
|
|
267
|
+
options.timing?.record('Spine: Initialized')
|
|
268
|
+
|
|
269
|
+
if (typeof spineOptions.spineFilesystemRoot === 'undefined') throw new Error(`'spineFilesystemRoot' not defined!`)
|
|
270
|
+
if (typeof spineOptions.spineContracts === 'undefined') throw new Error(`'spineContracts' not defined!`)
|
|
271
|
+
|
|
272
|
+
const spineContracts = spineOptions.spineContracts
|
|
273
|
+
const capsules: Record<string, any> = {}
|
|
274
|
+
|
|
275
|
+
options.timing?.record('Spine: Ready to encapsulate')
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
capsules,
|
|
279
|
+
freeze: async function (): Promise<TSpineSnapshot> {
|
|
280
|
+
|
|
281
|
+
options.timing?.record('Spine: Starting freeze')
|
|
282
|
+
|
|
283
|
+
const snapshot: TSpineSnapshot = {
|
|
284
|
+
capsules: {}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
options.timing?.record(`Spine: Freezing ${Object.keys(capsules).length} capsules`)
|
|
288
|
+
|
|
289
|
+
await Promise.all(Object.entries(capsules).map(async ([capsuleSourceLineRef, capsule]) => {
|
|
290
|
+
|
|
291
|
+
if (!capsule.cst.source.capsuleName) throw new Error(`'capsuleName' must be set for encapsulate options to enable freezing.`)
|
|
292
|
+
|
|
293
|
+
snapshot.capsules[capsuleSourceLineRef] = {
|
|
294
|
+
cst: capsule.cst,
|
|
295
|
+
spineContracts: {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { spineContractCapsuleInstances } = await capsule.makeInstance()
|
|
299
|
+
|
|
300
|
+
await Promise.all(Object.entries(spineContractCapsuleInstances).map(async ([spineContractUri, spineContractCapsuleInstance]) => {
|
|
301
|
+
|
|
302
|
+
snapshot.capsules[capsuleSourceLineRef] = merge(
|
|
303
|
+
snapshot.capsules[capsuleSourceLineRef],
|
|
304
|
+
await (spineContractCapsuleInstance as any).freeze({
|
|
305
|
+
spineContractUri,
|
|
306
|
+
capsule
|
|
307
|
+
})
|
|
308
|
+
)
|
|
309
|
+
}))
|
|
310
|
+
}))
|
|
311
|
+
|
|
312
|
+
options.timing?.record('Spine: Freeze complete')
|
|
313
|
+
|
|
314
|
+
// console.log('snapshot:', JSON.stringify(snapshot, null, 4))
|
|
315
|
+
return snapshot
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
encapsulate: async function (definition: TCapsuleDefinition, options: TCapsuleOptions): Promise<TCapsule> {
|
|
319
|
+
|
|
320
|
+
return encapsulate(definition, options, {
|
|
321
|
+
spineOptions,
|
|
322
|
+
spineContracts,
|
|
323
|
+
capsules
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
// ##################################################
|
|
333
|
+
// # Encapsulate
|
|
334
|
+
// ##################################################
|
|
335
|
+
|
|
336
|
+
async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOptions, spine: TSpineContext): Promise<TCapsule> {
|
|
337
|
+
|
|
338
|
+
if (!options.importMeta && !options.moduleFilepath) throw new Error(`'options.importMeta' nor 'options.moduleFilepath' not specified!`)
|
|
339
|
+
if (!options.importStack && !options.importStackLine) throw new Error(`'options.importStack' nor 'options.importStackLine' specified!`)
|
|
340
|
+
|
|
341
|
+
const moduleFilepath = options.moduleFilepath || relative(spine.spineOptions.spineFilesystemRoot || '', options.importMeta!.url.replace(/^file:\/\//, ''))
|
|
342
|
+
const importStackLine = options.importStackLine || formatImportStackFrame(options.importStack!)
|
|
343
|
+
|
|
344
|
+
if (typeof importStackLine !== 'number') throw new Error(`Could not determine importStackLine from options`)
|
|
345
|
+
|
|
346
|
+
const encapsulateOptions: TEncapsulateOptions = {
|
|
347
|
+
moduleFilepath,
|
|
348
|
+
importStackLine,
|
|
349
|
+
capsuleName: options.capsuleName,
|
|
350
|
+
ambientReferences: options.ambientReferences,
|
|
351
|
+
capsuleSourceLineRef: `${moduleFilepath}:${importStackLine}`
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
spine.spineOptions.timing?.record(`Encapsulate: Start for ${moduleFilepath}`)
|
|
355
|
+
|
|
356
|
+
const { csts, crts } = await spine.spineOptions.staticAnalyzer?.parseModule({
|
|
357
|
+
spineOptions: spine.spineOptions,
|
|
358
|
+
encapsulateOptions
|
|
359
|
+
}) || {
|
|
360
|
+
csts: options.cst ? { [encapsulateOptions.capsuleSourceLineRef]: options.cst } : undefined,
|
|
361
|
+
crts: options.crt ? { [encapsulateOptions.capsuleSourceLineRef]: options.crt } : undefined
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const defaultInstance: Record<string, any> = {}
|
|
365
|
+
|
|
366
|
+
// Cache for instances to prevent duplicate makeInstance calls
|
|
367
|
+
const instanceCache = new Map<string, Promise<any>>()
|
|
368
|
+
|
|
369
|
+
const capsule: TCapsule = {
|
|
370
|
+
toCapsuleReference: () => {
|
|
371
|
+
return {
|
|
372
|
+
capsuleSourceLineRef: encapsulateOptions.capsuleSourceLineRef,
|
|
373
|
+
capsuleSourceNameRefHash: capsule.cst.capsuleSourceNameRefHash,
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
capsuleSourceLineRef: encapsulateOptions.capsuleSourceLineRef,
|
|
377
|
+
definition,
|
|
378
|
+
encapsulateOptions,
|
|
379
|
+
cst: csts?.[encapsulateOptions.capsuleSourceLineRef],
|
|
380
|
+
crt: crts?.[encapsulateOptions.capsuleSourceLineRef],
|
|
381
|
+
makeInstance: async ({ overrides = {}, options = {}, runtimeSpineContracts }: TCapsuleMakeInstanceOptions = {}) => {
|
|
382
|
+
|
|
383
|
+
// Create cache key based on parameters
|
|
384
|
+
const cacheKey = JSON.stringify({
|
|
385
|
+
overrides,
|
|
386
|
+
options,
|
|
387
|
+
hasRuntimeContracts: !!runtimeSpineContracts
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
// Check if we already have a pending or completed instance creation
|
|
391
|
+
if (instanceCache.has(cacheKey)) {
|
|
392
|
+
return instanceCache.get(cacheKey)!
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Create the instance promise and cache it immediately
|
|
396
|
+
const instancePromise = (async () => {
|
|
397
|
+
const encapsulatedApi: Record<string, any> = {}
|
|
398
|
+
const spineContractCapsuleInstances: Record<string, any> = {}
|
|
399
|
+
|
|
400
|
+
const capsuleInstance = {
|
|
401
|
+
api: encapsulatedApi,
|
|
402
|
+
spineContractCapsuleInstances
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Property contracts are keys starting with '#' that contain nested properties
|
|
406
|
+
// Structure: spineContractUri -> propertyContractUri -> propertyName -> propertyDef
|
|
407
|
+
const propertyContractDefinitions: Record<string, Record<string, Record<string, any>>> = {}
|
|
408
|
+
// Track which property contracts are defined for validation
|
|
409
|
+
const definedPropertyContracts = new Set<string>()
|
|
410
|
+
|
|
411
|
+
for (const [spineContractUri, propertyDefinitions] of Object.entries(definition)) {
|
|
412
|
+
// Validate that at least one property contract key exists
|
|
413
|
+
const hasPropertyContract = Object.keys(propertyDefinitions).some(key => key.startsWith('#'))
|
|
414
|
+
if (!hasPropertyContract) {
|
|
415
|
+
throw new Error(`Spine contract '${spineContractUri}' for capsule '${capsule.capsuleSourceLineRef}' must specify at least one property contract layer using a key starting with '#'. For example: '#': { ...properties }`)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
propertyContractDefinitions[spineContractUri] = {}
|
|
419
|
+
|
|
420
|
+
for (const [propContractUri, propDef] of Object.entries(propertyDefinitions)) {
|
|
421
|
+
if (propContractUri.startsWith('#')) {
|
|
422
|
+
|
|
423
|
+
// This is a property contract - store its properties (merge if it already exists)
|
|
424
|
+
if (!propertyContractDefinitions[spineContractUri][propContractUri]) {
|
|
425
|
+
propertyContractDefinitions[spineContractUri][propContractUri] = {}
|
|
426
|
+
}
|
|
427
|
+
Object.assign(propertyContractDefinitions[spineContractUri][propContractUri], propDef as Record<string, any>)
|
|
428
|
+
definedPropertyContracts.add(propContractUri)
|
|
429
|
+
|
|
430
|
+
// If this is a non-default property contract, add a dynamic mapping to the '#' contract
|
|
431
|
+
if (propContractUri !== '#') {
|
|
432
|
+
// We have a property contract URI we need to resolve and load
|
|
433
|
+
// Add a dynamic property mapping for this contract to the '#' group
|
|
434
|
+
const contractKey = '#' + propContractUri.substring(1)
|
|
435
|
+
|
|
436
|
+
if (!propertyContractDefinitions[spineContractUri]['#']) {
|
|
437
|
+
propertyContractDefinitions[spineContractUri]['#'] = {}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
propertyContractDefinitions[spineContractUri]['#'][contractKey] = {
|
|
441
|
+
type: CapsulePropertyTypes.Mapping,
|
|
442
|
+
value: propContractUri.substring(1),
|
|
443
|
+
propertyContractDelegate: propContractUri
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
} else {
|
|
448
|
+
throw new Error(`Property '${propContractUri}' in spine contract '${spineContractUri}' for capsule '${capsule.capsuleSourceLineRef}' must be nested under a property contract uri starting with '#'. For example: '#': { ${propContractUri}: {...} }`)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Merge overrides and options by property contract
|
|
454
|
+
// Structure: propertyContractUri -> propertyName -> value
|
|
455
|
+
const mergedValuesByContract: Record<string, Record<string, any>> = {}
|
|
456
|
+
|
|
457
|
+
// Helper to validate and merge values from a source (overrides or options)
|
|
458
|
+
const mergeByContract = (source: any, sourceName: string) => {
|
|
459
|
+
if (!source) return
|
|
460
|
+
|
|
461
|
+
for (const [propertyContractUri, properties] of Object.entries(source)) {
|
|
462
|
+
if (!propertyContractUri.startsWith('#')) {
|
|
463
|
+
throw new Error(`${sourceName} for capsule '${capsule.capsuleSourceLineRef}' must use property contract keys starting with '#'. Found key: '${propertyContractUri}'`)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!definedPropertyContracts.has(propertyContractUri)) {
|
|
467
|
+
throw new Error(`${sourceName} for capsule '${capsule.capsuleSourceLineRef}' references property contract '${propertyContractUri}' which is not defined on the capsule`)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!mergedValuesByContract[propertyContractUri]) {
|
|
471
|
+
mergedValuesByContract[propertyContractUri] = {}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
Object.assign(mergedValuesByContract[propertyContractUri], properties)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Merge in order: overrides by lineRef, overrides by name, options
|
|
479
|
+
mergeByContract(overrides?.[encapsulateOptions.capsuleSourceLineRef], 'Overrides')
|
|
480
|
+
if (encapsulateOptions.capsuleName) {
|
|
481
|
+
mergeByContract(overrides?.[encapsulateOptions.capsuleName], 'Overrides')
|
|
482
|
+
}
|
|
483
|
+
mergeByContract(options, 'Options')
|
|
484
|
+
|
|
485
|
+
// Create a single shared self for all spine contracts by flattening merged values
|
|
486
|
+
const self = merge(
|
|
487
|
+
{},
|
|
488
|
+
defaultInstance,
|
|
489
|
+
...Object.values(mergedValuesByContract)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
// Use runtime spine contracts if provided, otherwise fall back to encapsulation spine contracts
|
|
493
|
+
const activeSpineContracts = runtimeSpineContracts || spine.spineContracts
|
|
494
|
+
|
|
495
|
+
for (const [spineContractUri, propertyContracts] of Object.entries(propertyContractDefinitions)) {
|
|
496
|
+
|
|
497
|
+
const spineContract = activeSpineContracts[spineContractUri] as any
|
|
498
|
+
|
|
499
|
+
if (!spineContract) throw new Error(`Contract uri '${spineContractUri}' used by capsule not available in Spine!`)
|
|
500
|
+
|
|
501
|
+
const spineContractCapsuleInstance = spineContract.makeContractCapsuleInstance({
|
|
502
|
+
spineContractUri,
|
|
503
|
+
encapsulateOptions,
|
|
504
|
+
capsuleInstance,
|
|
505
|
+
self,
|
|
506
|
+
capsule,
|
|
507
|
+
encapsulatedApi,
|
|
508
|
+
runtimeSpineContracts
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
spineContractCapsuleInstances[spineContractUri] = spineContractCapsuleInstance
|
|
512
|
+
|
|
513
|
+
// Iterate through each property contract within this spine contract
|
|
514
|
+
for (const [propertyContractUri, properties] of Object.entries(propertyContracts)) {
|
|
515
|
+
// Skip non-'#' property contracts as they're already accessible via dynamic mappings in '#'
|
|
516
|
+
if (propertyContractUri !== '#') {
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
for (const [propertyName, propertyDefinition] of Object.entries(properties)) {
|
|
520
|
+
|
|
521
|
+
if (!propertyDefinition.type || !(propertyDefinition.type in CapsulePropertyTypes)) throw new Error(`Type '${propertyDefinition.type}' for property '${propertyName}' on spineContract '${spineContractUri}' not set or supported!`)
|
|
522
|
+
|
|
523
|
+
await spineContractCapsuleInstance.mapProperty({
|
|
524
|
+
overrides,
|
|
525
|
+
options,
|
|
526
|
+
property: {
|
|
527
|
+
name: propertyName,
|
|
528
|
+
definition: propertyDefinition,
|
|
529
|
+
propertyContractUri
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return capsuleInstance
|
|
537
|
+
})()
|
|
538
|
+
|
|
539
|
+
// Cache the promise
|
|
540
|
+
instanceCache.set(cacheKey, instancePromise)
|
|
541
|
+
|
|
542
|
+
return instancePromise
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
spine.capsules[encapsulateOptions.capsuleSourceLineRef] = capsule
|
|
547
|
+
if (encapsulateOptions.capsuleName) {
|
|
548
|
+
spine.capsules[encapsulateOptions.capsuleName] = capsule
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
spine.spineOptions.timing?.record(`Encapsulate: Complete for ${moduleFilepath}`)
|
|
552
|
+
|
|
553
|
+
return capsule
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
function formatImportStackFrame(importStack: string): number | undefined {
|
|
559
|
+
const stackLines = importStack.split('\n')
|
|
560
|
+
|
|
561
|
+
const hasMakeImportStackMarker = importStack.includes('encapsulate:makeImportStack')
|
|
562
|
+
const targetMatchCount = hasMakeImportStackMarker ? 2 : 1
|
|
563
|
+
|
|
564
|
+
let matchCount = 0
|
|
565
|
+
for (let i = 0; i < stackLines.length; i++) {
|
|
566
|
+
const line = stackLines[i]
|
|
567
|
+
|
|
568
|
+
if (line.includes('encapsulate:makeImportStack')) {
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const match = line.match(/\(([^)]+):([0-9]+):[0-9]+\)|at ([^(]+):([0-9]+):[0-9]+/)
|
|
573
|
+
if (match) {
|
|
574
|
+
matchCount++
|
|
575
|
+
if (matchCount === targetMatchCount) {
|
|
576
|
+
const lineNumber = parseInt(match[2] || match[4])
|
|
577
|
+
return lineNumber
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return undefined
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ##################################################
|
|
586
|
+
// # Utilities
|
|
587
|
+
// ##################################################
|
|
588
|
+
|
|
589
|
+
export function makeImportStack() {
|
|
590
|
+
return new Error('encapsulate:makeImportStack').stack
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function join(...paths: string[]): string {
|
|
594
|
+
if (paths.length === 0) return '.'
|
|
595
|
+
|
|
596
|
+
let joined = paths.join('/')
|
|
597
|
+
|
|
598
|
+
const isAbsolute = joined.startsWith('/')
|
|
599
|
+
const parts: string[] = []
|
|
600
|
+
|
|
601
|
+
for (const part of joined.split('/')) {
|
|
602
|
+
if (part === '' || part === '.') continue
|
|
603
|
+
if (part === '..') {
|
|
604
|
+
if (parts.length > 0 && parts[parts.length - 1] !== '..') {
|
|
605
|
+
parts.pop()
|
|
606
|
+
} else if (!isAbsolute) {
|
|
607
|
+
parts.push('..')
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
parts.push(part)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let result = parts.join('/')
|
|
615
|
+
if (isAbsolute) result = '/' + result
|
|
616
|
+
|
|
617
|
+
return result || (isAbsolute ? '/' : '.')
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function relative(from: string, to: string): string {
|
|
621
|
+
const fromParts = from.split('/').filter(p => p && p !== '.')
|
|
622
|
+
const toParts = to.split('/').filter(p => p && p !== '.')
|
|
623
|
+
|
|
624
|
+
let commonLength = 0
|
|
625
|
+
const minLength = Math.min(fromParts.length, toParts.length)
|
|
626
|
+
|
|
627
|
+
for (let i = 0; i < minLength; i++) {
|
|
628
|
+
if (fromParts[i] === toParts[i]) {
|
|
629
|
+
commonLength++
|
|
630
|
+
} else {
|
|
631
|
+
break
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const upCount = fromParts.length - commonLength
|
|
636
|
+
const remainingTo = toParts.slice(commonLength)
|
|
637
|
+
|
|
638
|
+
const result = [...Array(upCount).fill('..'), ...remainingTo].join('/')
|
|
639
|
+
return result || '.'
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function isObject(item: any): boolean {
|
|
643
|
+
return item && typeof item === 'object' && !Array.isArray(item)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function merge<T = any>(target: T, ...sources: any[]): T {
|
|
647
|
+
if (!sources.length) return target
|
|
648
|
+
const source = sources.shift()
|
|
649
|
+
|
|
650
|
+
if (isObject(target) && isObject(source)) {
|
|
651
|
+
for (const key in source) {
|
|
652
|
+
if (isObject(source[key])) {
|
|
653
|
+
if (!target[key as keyof T]) Object.assign(target, { [key]: {} })
|
|
654
|
+
merge(target[key as keyof T], source[key])
|
|
655
|
+
} else {
|
|
656
|
+
Object.assign(target, { [key]: source[key] })
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return merge(target, ...sources)
|
|
662
|
+
}
|