@stream44.studio/encapsulate 0.2.0-rc.2 → 0.2.0-rc.4
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.
Potentially problematic release.
This version of @stream44.studio/encapsulate might be problematic. Click here for more details.
- package/LICENSE.md +4 -199
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +81 -72
- package/src/encapsulate.ts +175 -25
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +207 -135
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +131 -37
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +53 -6
- package/src/static-analyzer.v0.ts +291 -52
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CapsulePropertyTypes, join } from "../../encapsulate"
|
|
2
2
|
|
|
3
|
+
// Type for capsule instance registry - scoped per spine contract instance
|
|
4
|
+
export type CapsuleInstanceRegistry = Map<string, any>
|
|
5
|
+
|
|
3
6
|
export class ContractCapsuleInstanceFactory {
|
|
4
7
|
|
|
5
8
|
protected spineContractUri: string
|
|
@@ -10,16 +13,24 @@ export class ContractCapsuleInstanceFactory {
|
|
|
10
13
|
protected importCapsule?: (filepath: string) => Promise<any>
|
|
11
14
|
protected spineFilesystemRoot?: string
|
|
12
15
|
protected freezeCapsule?: (capsule: any) => Promise<any>
|
|
16
|
+
protected instanceRegistry?: CapsuleInstanceRegistry
|
|
17
|
+
protected extendedCapsuleInstance?: any
|
|
18
|
+
protected ownSelf?: any
|
|
19
|
+
protected runtimeSpineContracts?: Record<string, any>
|
|
13
20
|
|
|
14
|
-
constructor({ spineContractUri, capsule, self, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule }: { spineContractUri: string, capsule: any, self: any, encapsulatedApi: Record<string, any>, resolve?: (uri: string, parentFilepath: string) => Promise<string>, importCapsule?: (filepath: string) => Promise<any>, spineFilesystemRoot?: string, freezeCapsule?: (capsule: any) => Promise<any> }) {
|
|
21
|
+
constructor({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, runtimeSpineContracts }: { spineContractUri: string, capsule: any, self: any, ownSelf?: any, encapsulatedApi: Record<string, any>, resolve?: (uri: string, parentFilepath: string) => Promise<string>, importCapsule?: (filepath: string) => Promise<any>, spineFilesystemRoot?: string, freezeCapsule?: (capsule: any) => Promise<any>, instanceRegistry?: CapsuleInstanceRegistry, extendedCapsuleInstance?: any, runtimeSpineContracts?: Record<string, any> }) {
|
|
15
22
|
this.spineContractUri = spineContractUri
|
|
16
23
|
this.capsule = capsule
|
|
17
24
|
this.self = self
|
|
25
|
+
this.ownSelf = ownSelf
|
|
18
26
|
this.encapsulatedApi = encapsulatedApi
|
|
19
27
|
this.resolve = resolve
|
|
20
28
|
this.importCapsule = importCapsule
|
|
21
29
|
this.spineFilesystemRoot = spineFilesystemRoot
|
|
22
30
|
this.freezeCapsule = freezeCapsule
|
|
31
|
+
this.instanceRegistry = instanceRegistry
|
|
32
|
+
this.extendedCapsuleInstance = extendedCapsuleInstance
|
|
33
|
+
this.runtimeSpineContracts = runtimeSpineContracts
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
async mapProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
|
|
@@ -59,7 +70,11 @@ export class ContractCapsuleInstanceFactory {
|
|
|
59
70
|
if (!this.spineFilesystemRoot) throw new Error(`'spineFilesystemRoot' not set!`)
|
|
60
71
|
if (!this.importCapsule) throw new Error(`'importCapsule' not set!`)
|
|
61
72
|
|
|
62
|
-
|
|
73
|
+
// Use encapsulateOptions.moduleFilepath (always available) instead of cst.source.moduleFilepath
|
|
74
|
+
const moduleFilepath = this.capsule.encapsulateOptions?.moduleFilepath || this.capsule.cst?.source?.moduleFilepath
|
|
75
|
+
if (!moduleFilepath) throw new Error(`'moduleFilepath' not available on capsule!`)
|
|
76
|
+
|
|
77
|
+
const parentPath = join(this.spineFilesystemRoot, moduleFilepath)
|
|
63
78
|
const filepath = await this.resolve(property.definition.value, parentPath)
|
|
64
79
|
mappedCapsule = await this.importCapsule(filepath)
|
|
65
80
|
} else if (
|
|
@@ -121,9 +136,63 @@ export class ContractCapsuleInstanceFactory {
|
|
|
121
136
|
const mappedCapsule = await this.resolveMappedCapsule({ property })
|
|
122
137
|
const constants = await this.extractConstants({ mappedCapsule })
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
139
|
+
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
140
|
+
// options can be a function or an object for regular mappings
|
|
141
|
+
const mappingOptions = property.definition.delegateOptions
|
|
142
|
+
|| (typeof property.definition.options === 'function'
|
|
143
|
+
? await property.definition.options({ constants })
|
|
144
|
+
: property.definition.options)
|
|
145
|
+
|
|
146
|
+
// Check for existing instance in registry - reuse if available when no options
|
|
147
|
+
// Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
|
|
148
|
+
const capsuleName = mappedCapsule.encapsulateOptions?.capsuleName
|
|
149
|
+
|
|
150
|
+
if (capsuleName && this.instanceRegistry) {
|
|
151
|
+
if (this.instanceRegistry.has(capsuleName)) {
|
|
152
|
+
const existingEntry = this.instanceRegistry.get(capsuleName)
|
|
153
|
+
|
|
154
|
+
// Only reuse if current mapping has no options
|
|
155
|
+
if (!mappingOptions) {
|
|
156
|
+
// Use deferred proxy that resolves from registry when accessed
|
|
157
|
+
// Works for both null (pre-registered) and actual instances
|
|
158
|
+
const apiTarget = this.getApiTarget({ property })
|
|
159
|
+
const registry = this.instanceRegistry
|
|
160
|
+
apiTarget[property.name] = new Proxy({} as any, {
|
|
161
|
+
get: (_target: any, apiProp: string | symbol) => {
|
|
162
|
+
if (typeof apiProp === 'symbol') return undefined
|
|
163
|
+
const resolvedInstance = registry.get(capsuleName)
|
|
164
|
+
if (!resolvedInstance) {
|
|
165
|
+
throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
|
|
166
|
+
}
|
|
167
|
+
// Access through .api if it exists (for capsule instances with getters)
|
|
168
|
+
if (resolvedInstance.api && apiProp in resolvedInstance.api) {
|
|
169
|
+
return resolvedInstance.api[apiProp]
|
|
170
|
+
}
|
|
171
|
+
return resolvedInstance[apiProp]
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
this.self[property.name] = new Proxy({} as any, {
|
|
175
|
+
get: (_target, prop) => {
|
|
176
|
+
if (typeof prop === 'symbol') return undefined
|
|
177
|
+
const resolvedInstance = registry.get(capsuleName)
|
|
178
|
+
if (!resolvedInstance) {
|
|
179
|
+
throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
|
|
180
|
+
}
|
|
181
|
+
const value = resolvedInstance.api?.[prop] ?? resolvedInstance[prop]
|
|
182
|
+
if (value && typeof value === 'object' && value.api) {
|
|
183
|
+
return value.api
|
|
184
|
+
}
|
|
185
|
+
return value
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
// If current mapping has options, fall through to create new instance
|
|
191
|
+
} else {
|
|
192
|
+
// Pre-register as null to claim the slot for this capsule
|
|
193
|
+
this.instanceRegistry.set(capsuleName, null)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
127
196
|
|
|
128
197
|
// Transform overrides if this mapping has a propertyContractDelegate
|
|
129
198
|
let mappedOverrides = overrides
|
|
@@ -152,9 +221,16 @@ export class ContractCapsuleInstanceFactory {
|
|
|
152
221
|
const apiTarget = this.getApiTarget({ property })
|
|
153
222
|
const mappedInstance = await mappedCapsule.makeInstance({
|
|
154
223
|
overrides: mappedOverrides,
|
|
155
|
-
options: mappingOptions
|
|
224
|
+
options: mappingOptions,
|
|
225
|
+
runtimeSpineContracts: this.runtimeSpineContracts
|
|
156
226
|
})
|
|
157
227
|
|
|
228
|
+
// Register the instance (replaces null pre-registration marker)
|
|
229
|
+
// Always register to make instance available for child capsules with deferred proxies
|
|
230
|
+
if (capsuleName && this.instanceRegistry) {
|
|
231
|
+
this.instanceRegistry.set(capsuleName, mappedInstance)
|
|
232
|
+
}
|
|
233
|
+
|
|
158
234
|
apiTarget[property.name] = mappedInstance
|
|
159
235
|
// Use proxy to unwrap .api for this.self so internal references work
|
|
160
236
|
this.self[property.name] = mappedInstance.api ? new Proxy(mappedInstance.api, {
|
|
@@ -186,24 +262,33 @@ export class ContractCapsuleInstanceFactory {
|
|
|
186
262
|
|
|
187
263
|
protected mapLiteralProperty({ property }: { property: any }) {
|
|
188
264
|
const apiTarget = this.getApiTarget({ property })
|
|
189
|
-
|
|
190
|
-
|
|
265
|
+
// Use existing value from self if defined, otherwise use property definition
|
|
266
|
+
// This preserves values set by child capsules in the extends chain
|
|
267
|
+
const existingValue = this.self[property.name]
|
|
268
|
+
const value = existingValue !== undefined
|
|
269
|
+
? existingValue
|
|
191
270
|
: property.definition.value
|
|
192
271
|
|
|
193
272
|
// Assign to both apiTarget and self so getter functions can access via this
|
|
194
273
|
apiTarget[property.name] = value
|
|
195
|
-
|
|
274
|
+
// Only update self if it wasn't already set (preserve child values)
|
|
275
|
+
if (existingValue === undefined) {
|
|
276
|
+
this.self[property.name] = value
|
|
277
|
+
}
|
|
196
278
|
}
|
|
197
279
|
|
|
198
|
-
protected
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Prefer this.self (which has unwrapped APIs) over encapsulatedApi
|
|
203
|
-
const selfProxy = new Proxy(this.self, {
|
|
280
|
+
protected createSelfProxy() {
|
|
281
|
+
const extendedApi = this.extendedCapsuleInstance?.api
|
|
282
|
+
const ownSelf = this.ownSelf
|
|
283
|
+
return new Proxy(this.self, {
|
|
204
284
|
get: (target: any, prop: string | symbol) => {
|
|
205
285
|
if (typeof prop === 'symbol') return target[prop]
|
|
206
286
|
|
|
287
|
+
// 'self' property returns ownSelf (only this capsule's own properties)
|
|
288
|
+
if (prop === 'self' && ownSelf) {
|
|
289
|
+
return ownSelf
|
|
290
|
+
}
|
|
291
|
+
|
|
207
292
|
// First check if the property exists in target (this.self)
|
|
208
293
|
if (prop in target) {
|
|
209
294
|
return target[prop]
|
|
@@ -214,36 +299,26 @@ export class ContractCapsuleInstanceFactory {
|
|
|
214
299
|
return this.encapsulatedApi[prop]
|
|
215
300
|
}
|
|
216
301
|
|
|
302
|
+
// Fall back to extended capsule's API
|
|
303
|
+
if (extendedApi && prop in extendedApi) {
|
|
304
|
+
return extendedApi[prop]
|
|
305
|
+
}
|
|
306
|
+
|
|
217
307
|
return undefined
|
|
218
308
|
}
|
|
219
309
|
})
|
|
310
|
+
}
|
|
220
311
|
|
|
312
|
+
protected mapFunctionProperty({ property }: { property: any }) {
|
|
313
|
+
const apiTarget = this.getApiTarget({ property })
|
|
314
|
+
const selfProxy = this.createSelfProxy()
|
|
221
315
|
apiTarget[property.name] = property.definition.value.bind(selfProxy)
|
|
222
316
|
}
|
|
223
317
|
|
|
224
318
|
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
225
319
|
const apiTarget = this.getApiTarget({ property })
|
|
226
320
|
const getterFn = property.definition.value
|
|
227
|
-
|
|
228
|
-
// Create a proxy for this.self that intercepts property access
|
|
229
|
-
// Prefer this.self (which has unwrapped APIs) over encapsulatedApi
|
|
230
|
-
const selfProxy = new Proxy(this.self, {
|
|
231
|
-
get: (target: any, prop: string | symbol) => {
|
|
232
|
-
if (typeof prop === 'symbol') return target[prop]
|
|
233
|
-
|
|
234
|
-
// First check if the property exists in target (this.self)
|
|
235
|
-
if (prop in target) {
|
|
236
|
-
return target[prop]
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Fall back to encapsulatedApi
|
|
240
|
-
if (prop in this.encapsulatedApi) {
|
|
241
|
-
return this.encapsulatedApi[prop]
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return undefined
|
|
245
|
-
}
|
|
246
|
-
})
|
|
321
|
+
const selfProxy = this.createSelfProxy()
|
|
247
322
|
|
|
248
323
|
// Define a lazy getter that calls the function only when accessed with proper this context
|
|
249
324
|
Object.defineProperty(apiTarget, property.name, {
|
|
@@ -253,6 +328,18 @@ export class ContractCapsuleInstanceFactory {
|
|
|
253
328
|
enumerable: true,
|
|
254
329
|
configurable: true
|
|
255
330
|
})
|
|
331
|
+
|
|
332
|
+
// Also define the getter on ownSelf so this.self.propertyName works for getter functions
|
|
333
|
+
// This ensures this.self accesses the getter, not a raw value
|
|
334
|
+
if (this.ownSelf) {
|
|
335
|
+
Object.defineProperty(this.ownSelf, property.name, {
|
|
336
|
+
get: () => {
|
|
337
|
+
return getterFn.call(selfProxy)
|
|
338
|
+
},
|
|
339
|
+
enumerable: true,
|
|
340
|
+
configurable: true
|
|
341
|
+
})
|
|
342
|
+
}
|
|
256
343
|
}
|
|
257
344
|
|
|
258
345
|
async freeze(options: any): Promise<any> {
|
|
@@ -266,18 +353,25 @@ export class ContractCapsuleInstanceFactory {
|
|
|
266
353
|
|
|
267
354
|
export function CapsuleSpineContract({ freezeCapsule, resolve, importCapsule, spineFilesystemRoot }: { freezeCapsule?: (capsule: any) => Promise<any>, resolve?: (uri: string, parentFilepath: string) => Promise<string>, importCapsule?: (filepath: string) => Promise<any>, spineFilesystemRoot?: string } = {}) {
|
|
268
355
|
|
|
356
|
+
const instanceRegistry: CapsuleInstanceRegistry = new Map()
|
|
357
|
+
|
|
269
358
|
return {
|
|
270
359
|
'#': CapsuleSpineContract['#'],
|
|
271
|
-
|
|
360
|
+
instanceRegistry,
|
|
361
|
+
makeContractCapsuleInstance: ({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, extendedCapsuleInstance, runtimeSpineContracts }: { spineContractUri: string, capsule: any, self: any, ownSelf?: any, encapsulatedApi: Record<string, any>, extendedCapsuleInstance?: any, runtimeSpineContracts?: Record<string, any> }) => {
|
|
272
362
|
return new ContractCapsuleInstanceFactory({
|
|
273
363
|
spineContractUri,
|
|
274
364
|
capsule,
|
|
275
365
|
self,
|
|
366
|
+
ownSelf,
|
|
276
367
|
encapsulatedApi,
|
|
277
368
|
resolve,
|
|
278
369
|
importCapsule,
|
|
279
370
|
spineFilesystemRoot,
|
|
280
|
-
freezeCapsule
|
|
371
|
+
freezeCapsule,
|
|
372
|
+
instanceRegistry,
|
|
373
|
+
extendedCapsuleInstance,
|
|
374
|
+
runtimeSpineContracts
|
|
281
375
|
})
|
|
282
376
|
},
|
|
283
377
|
hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, dirname } from 'path'
|
|
2
2
|
import { writeFile, mkdir, readFile, stat } from 'fs/promises'
|
|
3
|
+
import { createRequire } from 'module'
|
|
3
4
|
import { Spine, SpineRuntime, CapsulePropertyTypes, makeImportStack, merge } from "../encapsulate"
|
|
4
5
|
import { StaticAnalyzer } from "../../src/static-analyzer.v0"
|
|
5
6
|
import { CapsuleModuleProjector } from "../../src/capsule-projectors/CapsuleModuleProjector.v0"
|
|
@@ -7,8 +8,49 @@ import { CapsuleModuleProjector } from "../../src/capsule-projectors/CapsuleModu
|
|
|
7
8
|
|
|
8
9
|
export { merge }
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
|
|
11
|
+
// Custom resolve function that uses createRequire for proper package resolution
|
|
12
|
+
async function resolve(uri: string, fromPath: string, spineRoot?: string): Promise<string> {
|
|
13
|
+
try {
|
|
14
|
+
// Create a require function from the parent file's directory
|
|
15
|
+
const require = createRequire(fromPath)
|
|
16
|
+
const result = require.resolve(uri)
|
|
17
|
+
return result
|
|
18
|
+
} catch (error: any) {
|
|
19
|
+
// If standard resolution fails and uri is a scoped package, try resolving
|
|
20
|
+
// the package root first, then append the subpath relative to it.
|
|
21
|
+
if (uri.startsWith('@')) {
|
|
22
|
+
const match = uri.match(/^(@[^/]+\/[^/]+)\/(.+)$/)
|
|
23
|
+
if (match) {
|
|
24
|
+
const [, packageName, subpath] = match
|
|
25
|
+
try {
|
|
26
|
+
const require = createRequire(fromPath)
|
|
27
|
+
const packageJsonPath = require.resolve(join(packageName, 'package.json'))
|
|
28
|
+
const packageRoot = dirname(packageJsonPath)
|
|
29
|
+
const fsPath = join(packageRoot, subpath + '.ts')
|
|
30
|
+
await stat(fsPath)
|
|
31
|
+
return fsPath
|
|
32
|
+
} catch {
|
|
33
|
+
// Fall through
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback: transform @scope/package/path to scope/packages/package/path relative to spineRoot
|
|
38
|
+
if (spineRoot) {
|
|
39
|
+
const transformed = uri.replace(/^@([^/]+)\/([^/]+)\/(.+)$/, '$1/packages/$2/$3')
|
|
40
|
+
const fsPath = join(spineRoot, transformed + '.ts')
|
|
41
|
+
try {
|
|
42
|
+
await stat(fsPath)
|
|
43
|
+
return fsPath
|
|
44
|
+
} catch {
|
|
45
|
+
// File doesn't exist, fall through to Bun.resolve
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Final fallback to Bun.resolve
|
|
51
|
+
return await Bun.resolve(uri, fromPath)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
12
54
|
|
|
13
55
|
|
|
14
56
|
export async function CapsuleSpineFactory({
|
|
@@ -67,10 +109,10 @@ export async function CapsuleSpineFactory({
|
|
|
67
109
|
resolve: async (uri: string, parentFilepath: string) => {
|
|
68
110
|
// For relative paths, join with parent directory first
|
|
69
111
|
if (/^\.\.?\//.test(uri)) {
|
|
70
|
-
return await resolve(join(parentFilepath, '..', uri), spineFilesystemRoot)
|
|
112
|
+
return await resolve(join(parentFilepath, '..', uri), spineFilesystemRoot, spineFilesystemRoot)
|
|
71
113
|
}
|
|
72
|
-
// For absolute/package paths, use
|
|
73
|
-
return await resolve(uri, parentFilepath)
|
|
114
|
+
// For absolute/package paths, use custom resolve with spine root
|
|
115
|
+
return await resolve(uri, parentFilepath, spineFilesystemRoot)
|
|
74
116
|
},
|
|
75
117
|
importCapsule: async (filepath: string) => {
|
|
76
118
|
const shortPath = filepath.replace(/^.*\/genesis\//, '')
|
|
@@ -126,11 +168,15 @@ export async function CapsuleSpineFactory({
|
|
|
126
168
|
|
|
127
169
|
let snapshotValues = {}
|
|
128
170
|
|
|
171
|
+
// Create a new set per freezeCapsule call to track circular dependencies within this projection tree
|
|
172
|
+
const projectingCapsules = new Set<string>()
|
|
173
|
+
|
|
129
174
|
const projected = await projector.projectCapsule({
|
|
130
175
|
capsule,
|
|
131
176
|
capsules,
|
|
132
177
|
snapshotValues,
|
|
133
|
-
spineContractUri
|
|
178
|
+
spineContractUri,
|
|
179
|
+
projectingCapsules
|
|
134
180
|
})
|
|
135
181
|
|
|
136
182
|
return snapshotValues
|
|
@@ -281,6 +327,7 @@ export async function CapsuleSpineFactory({
|
|
|
281
327
|
run,
|
|
282
328
|
freeze,
|
|
283
329
|
loadCapsule,
|
|
330
|
+
spineContractInstances, // Expose for testing
|
|
284
331
|
hoistSnapshot: async ({ snapshot }: { snapshot: any }) => {
|
|
285
332
|
|
|
286
333
|
timing?.recordMajor('HOIST SNAPSHOT: START')
|