@stream44.studio/encapsulate 0.4.0-rc.34 → 0.4.0-rc.39
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 +2 -2
- package/package.json +6 -5
- package/src/capsule-projectors/{CapsuleModuleProjector.v0.ts → CapsuleModuleProjector.ts} +52 -447
- package/src/encapsulate.ts +126 -20
- package/src/spine-contracts/CapsuleSpineContract.v0/{Membrane.v0.ts → Membrane.ts} +124 -77
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +29 -3
- package/src/spine-contracts/CapsuleSpineContract.v0/{Static.v0.ts → Static.ts} +144 -22
- package/src/spine-factories/{CapsuleSpineFactory.v0.ts → CapsuleSpineFactory.ts} +117 -6
- package/src/{static-analyzer.v0.ts → static-analyzer.ts} +15 -0
- package/structs/CapsuleProjectionContext.ts +53 -0
- package/structs/StructFactory.ts +90 -0
|
@@ -23,6 +23,7 @@ export class ContractCapsuleInstanceFactory {
|
|
|
23
23
|
public structDisposeFunctions: Array<() => any> = []
|
|
24
24
|
public initFunctions: Array<() => any> = []
|
|
25
25
|
public disposeFunctions: Array<() => any> = []
|
|
26
|
+
public onFreezeFunctions: Array<() => any> = []
|
|
26
27
|
public mappedCapsuleInstances: Array<any> = []
|
|
27
28
|
protected memoizeCache: Map<string, any> = new Map()
|
|
28
29
|
protected memoizeTimeouts: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
@@ -104,6 +105,10 @@ export class ContractCapsuleInstanceFactory {
|
|
|
104
105
|
this.mapInitProperty({ property })
|
|
105
106
|
} else if (property.definition.type === CapsulePropertyTypes.Dispose) {
|
|
106
107
|
this.mapDisposeProperty({ property })
|
|
108
|
+
} else if (property.definition.type === CapsulePropertyTypes.OnFreeze) {
|
|
109
|
+
this.mapOnFreezeProperty({ property })
|
|
110
|
+
} else if (property.definition.type === CapsulePropertyTypes.ProxyFunction) {
|
|
111
|
+
this.mapProxyFunctionProperty({ property })
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -131,11 +136,76 @@ export class ContractCapsuleInstanceFactory {
|
|
|
131
136
|
|
|
132
137
|
// Use cst.source.moduleFilepath (always filesystem-relative) for path resolution.
|
|
133
138
|
// encapsulateOptions.moduleFilepath may be an npm URI when loaded from projected files.
|
|
134
|
-
|
|
139
|
+
// However, cst.source.moduleFilepath may be relative to a different package root than
|
|
140
|
+
// the current spineFilesystemRoot (e.g. cross-package capsule references). In that case,
|
|
141
|
+
// fall back to encapsulateOptions.moduleFilepath which is always relative to current spine root.
|
|
142
|
+
let moduleFilepath = this.capsule.cst?.source?.moduleFilepath || this.capsule.encapsulateOptions?.moduleFilepath
|
|
135
143
|
if (!moduleFilepath) throw new Error(`'moduleFilepath' not available on capsule!`)
|
|
136
144
|
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
// moduleFilepath may be a relative filesystem path (e.g. "../../../caps/Foo.ts")
|
|
146
|
+
// or an npm-style URI (e.g. "@stream44.studio/t44-docker.com/caps/Project")
|
|
147
|
+
// after freeze/hoist cycles. join(spineRoot, npmUri) produces a bogus path when
|
|
148
|
+
// the URI is npm-style and spineRoot is narrow. Detect this by checking if the
|
|
149
|
+
// moduleFilepath looks like an npm URI (starts with @, or doesn't start with / or .),
|
|
150
|
+
// and resolve it to get the actual filesystem path.
|
|
151
|
+
let parentPath = join(this.spineFilesystemRoot, moduleFilepath)
|
|
152
|
+
if (!moduleFilepath.startsWith('/') && !moduleFilepath.startsWith('.')) {
|
|
153
|
+
// Looks like an npm-style URI or filesystem-convention path — resolve it
|
|
154
|
+
try {
|
|
155
|
+
// Try with @ prefix first (npm URI convention)
|
|
156
|
+
if (moduleFilepath.startsWith('@')) {
|
|
157
|
+
parentPath = await this.resolve(moduleFilepath, this.spineFilesystemRoot)
|
|
158
|
+
} else {
|
|
159
|
+
// May be a filesystem-convention path like "scope/packages/pkg/path"
|
|
160
|
+
// Try resolving as @scope/pkg/path by extracting scope and package name
|
|
161
|
+
const parts = moduleFilepath.split('/')
|
|
162
|
+
if (parts.length >= 3 && parts[1] === 'packages') {
|
|
163
|
+
const scope = parts[0]
|
|
164
|
+
const pkg = parts[2]
|
|
165
|
+
const subpath = parts.slice(3).join('/')
|
|
166
|
+
const npmUri = subpath ? `@${scope}/${pkg}/${subpath}` : `@${scope}/${pkg}`
|
|
167
|
+
parentPath = await this.resolve(npmUri, this.spineFilesystemRoot)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Fall back to the joined path if resolution fails
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let filepath: string
|
|
175
|
+
try {
|
|
176
|
+
filepath = await this.resolve(property.definition.value, parentPath)
|
|
177
|
+
} catch (resolveError) {
|
|
178
|
+
// cst.source.moduleFilepath may be relative to a different package root than spineFilesystemRoot
|
|
179
|
+
// (e.g. cross-package capsule references). Fall back to encapsulateOptions.moduleFilepath
|
|
180
|
+
// which is always relative to the current spine root.
|
|
181
|
+
const fallbackModuleFilepath = this.capsule.encapsulateOptions?.moduleFilepath
|
|
182
|
+
if (fallbackModuleFilepath && fallbackModuleFilepath !== moduleFilepath) {
|
|
183
|
+
// The fallback moduleFilepath may be a relative path or an npm URI.
|
|
184
|
+
// Apply the same resolution logic as the primary path.
|
|
185
|
+
let fallbackParentPath = join(this.spineFilesystemRoot, fallbackModuleFilepath)
|
|
186
|
+
if (!fallbackModuleFilepath.startsWith('/') && !fallbackModuleFilepath.startsWith('.')) {
|
|
187
|
+
try {
|
|
188
|
+
if (fallbackModuleFilepath.startsWith('@')) {
|
|
189
|
+
fallbackParentPath = await this.resolve!(fallbackModuleFilepath, this.spineFilesystemRoot)
|
|
190
|
+
} else {
|
|
191
|
+
const parts = fallbackModuleFilepath.split('/')
|
|
192
|
+
if (parts.length >= 3 && parts[1] === 'packages') {
|
|
193
|
+
const scope = parts[0]
|
|
194
|
+
const pkg = parts[2]
|
|
195
|
+
const subpath = parts.slice(3).join('/')
|
|
196
|
+
const npmUri = subpath ? `@${scope}/${pkg}/${subpath}` : `@${scope}/${pkg}`
|
|
197
|
+
fallbackParentPath = await this.resolve!(npmUri, this.spineFilesystemRoot)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// Fall back to the joined path
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
filepath = await this.resolve(property.definition.value, fallbackParentPath)
|
|
205
|
+
} else {
|
|
206
|
+
throw resolveError
|
|
207
|
+
}
|
|
208
|
+
}
|
|
139
209
|
mappedCapsule = await this.importCapsule(filepath)
|
|
140
210
|
} else if (
|
|
141
211
|
typeof property.definition.value === 'object' &&
|
|
@@ -212,9 +282,12 @@ export class ContractCapsuleInstanceFactory {
|
|
|
212
282
|
|
|
213
283
|
// Check for existing instance in registry - reuse if available when no options
|
|
214
284
|
// Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
|
|
215
|
-
// Property contract delegates (structs) always get a fresh instance per parent capsule
|
|
285
|
+
// Property contract delegates (structs) always get a fresh instance per parent capsule.
|
|
286
|
+
// CapsuleProjectionContext also needs fresh instances so context injection can find it
|
|
287
|
+
// in mappedCapsuleInstances during freeze traversal.
|
|
216
288
|
const capsuleName = mappedCapsule.encapsulateOptions?.capsuleName
|
|
217
289
|
const isCapsuleStruct = property.definition.propertyContractDelegate === '#@stream44.studio/encapsulate/structs/Capsule'
|
|
290
|
+
|| property.definition.propertyContractDelegate === '#@stream44.studio/encapsulate/structs/CapsuleProjectionContext'
|
|
218
291
|
|
|
219
292
|
if (capsuleName && this.instanceRegistry && !isCapsuleStruct) {
|
|
220
293
|
if (this.instanceRegistry.has(capsuleName)) {
|
|
@@ -335,6 +408,10 @@ export class ContractCapsuleInstanceFactory {
|
|
|
335
408
|
}
|
|
336
409
|
|
|
337
410
|
apiTarget[property.name] = mappedInstance
|
|
411
|
+
mappedInstance.mappedPropertyName = property.name
|
|
412
|
+
if (property.definition.propertyContractDelegate) {
|
|
413
|
+
mappedInstance.isPropertyContractDelegate = true
|
|
414
|
+
}
|
|
338
415
|
this.mappedCapsuleInstances.push(mappedInstance)
|
|
339
416
|
// Use proxy to unwrap .api for this.self so internal references work
|
|
340
417
|
this.self[property.name] = mappedInstance.api ? new Proxy(mappedInstance.api, {
|
|
@@ -349,18 +426,17 @@ export class ContractCapsuleInstanceFactory {
|
|
|
349
426
|
}) : mappedInstance
|
|
350
427
|
|
|
351
428
|
// If this mapping has a propertyContractDelegate, also mount the mapped capsule's API
|
|
352
|
-
// to the property contract namespace for direct access
|
|
429
|
+
// to the property contract namespace for direct access.
|
|
430
|
+
// Use a proxy so that later mutations to the delegate's API (e.g. CapsuleProjectionContext
|
|
431
|
+
// injection during freeze) are visible through the parent's encapsulatedApi.
|
|
353
432
|
if (property.definition.propertyContractDelegate) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
for (const [key, value] of Object.entries(mappedInstance.api)) {
|
|
362
|
-
delegateTarget[key] = value
|
|
363
|
-
}
|
|
433
|
+
this.encapsulatedApi[property.definition.propertyContractDelegate] = new Proxy(mappedInstance.api, {
|
|
434
|
+
get: (target: any, prop: string | symbol) => target[prop],
|
|
435
|
+
set: (target: any, prop: string | symbol, value: any) => { target[prop] = value; return true },
|
|
436
|
+
ownKeys: (target: any) => Reflect.ownKeys(target),
|
|
437
|
+
getOwnPropertyDescriptor: (target: any, prop: string | symbol) => Object.getOwnPropertyDescriptor(target, prop) || { configurable: true, enumerable: true, writable: true, value: target[prop] },
|
|
438
|
+
has: (target: any, prop: string | symbol) => prop in target,
|
|
439
|
+
})
|
|
364
440
|
}
|
|
365
441
|
}
|
|
366
442
|
|
|
@@ -396,21 +472,29 @@ export class ContractCapsuleInstanceFactory {
|
|
|
396
472
|
|
|
397
473
|
// First check if the property exists in target (this.self)
|
|
398
474
|
if (prop in target) {
|
|
475
|
+
// Virtual dispatch: if child has an override for this property,
|
|
476
|
+
// prefer the child's version over self (which may hold the parent's bound fn)
|
|
477
|
+
if (factory.childEncapsulatedApis) {
|
|
478
|
+
for (const childApi of factory.childEncapsulatedApis) {
|
|
479
|
+
if (prop in childApi) return childApi[prop]
|
|
480
|
+
}
|
|
481
|
+
}
|
|
399
482
|
return target[prop]
|
|
400
483
|
}
|
|
401
484
|
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
return factory.encapsulatedApi[prop]
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Fall back to child capsule APIs (for parent→child function delegation)
|
|
485
|
+
// Check child capsule APIs (virtual dispatch —
|
|
486
|
+
// child overrides take precedence over parent's own API)
|
|
408
487
|
if (factory.childEncapsulatedApis) {
|
|
409
488
|
for (const childApi of factory.childEncapsulatedApis) {
|
|
410
489
|
if (prop in childApi) return childApi[prop]
|
|
411
490
|
}
|
|
412
491
|
}
|
|
413
492
|
|
|
493
|
+
// Fall back to own encapsulatedApi
|
|
494
|
+
if (prop in factory.encapsulatedApi) {
|
|
495
|
+
return factory.encapsulatedApi[prop]
|
|
496
|
+
}
|
|
497
|
+
|
|
414
498
|
// Fall back to extended capsule's API
|
|
415
499
|
if (extendedApi && prop in extendedApi) {
|
|
416
500
|
return extendedApi[prop]
|
|
@@ -438,12 +522,12 @@ export class ContractCapsuleInstanceFactory {
|
|
|
438
522
|
getOwnPropertyDescriptor: (target: any, prop: string | symbol) => {
|
|
439
523
|
if (typeof prop === 'symbol') return Object.getOwnPropertyDescriptor(target, prop)
|
|
440
524
|
if (prop in target) return Object.getOwnPropertyDescriptor(target, prop)
|
|
441
|
-
if (prop in factory.encapsulatedApi) return { configurable: true, enumerable: true, writable: true, value: factory.encapsulatedApi[prop as string] }
|
|
442
525
|
if (factory.childEncapsulatedApis) {
|
|
443
526
|
for (const childApi of factory.childEncapsulatedApis) {
|
|
444
527
|
if (prop in childApi) return { configurable: true, enumerable: true, writable: true, value: childApi[prop as string] }
|
|
445
528
|
}
|
|
446
529
|
}
|
|
530
|
+
if (prop in factory.encapsulatedApi) return { configurable: true, enumerable: true, writable: true, value: factory.encapsulatedApi[prop as string] }
|
|
447
531
|
if (extendedApi && prop in extendedApi) return { configurable: true, enumerable: true, writable: true, value: extendedApi[prop as string] }
|
|
448
532
|
return undefined
|
|
449
533
|
}
|
|
@@ -556,6 +640,38 @@ export class ContractCapsuleInstanceFactory {
|
|
|
556
640
|
}
|
|
557
641
|
}
|
|
558
642
|
|
|
643
|
+
protected mapProxyFunctionProperty({ property }: { property: any }) {
|
|
644
|
+
const apiTarget = this.getApiTarget({ property })
|
|
645
|
+
const selfProxy = this.createSelfProxy()
|
|
646
|
+
|
|
647
|
+
const childTargetFn = property.definition.value.target
|
|
648
|
+
const childInvokeFn = property.definition.value.invoke
|
|
649
|
+
|
|
650
|
+
// Inherit missing parts from parent's ProxyFunction (if extending)
|
|
651
|
+
const parentParts = this.self[`__proxyFn_${property.name}`]
|
|
652
|
+
const targetFn = childTargetFn ?? parentParts?.target
|
|
653
|
+
const invokeFn = childInvokeFn ?? parentParts?.invoke
|
|
654
|
+
|
|
655
|
+
if (!targetFn) throw new Error(`ProxyFunction '${property.name}': target() is required`)
|
|
656
|
+
if (!invokeFn) throw new Error(`ProxyFunction '${property.name}': invoke() is required`)
|
|
657
|
+
|
|
658
|
+
// Store parts for potential child override
|
|
659
|
+
this.self[`__proxyFn_${property.name}`] = { target: targetFn, invoke: invokeFn }
|
|
660
|
+
|
|
661
|
+
apiTarget[property.name] = (...args: any[]) => {
|
|
662
|
+
// 1. Call invoke() bound to selfProxy to transform args
|
|
663
|
+
const transformedArgs = invokeFn.call(selfProxy, ...args)
|
|
664
|
+
// 2. Call target() bound to selfProxy to get the function to call
|
|
665
|
+
const target = targetFn.call(selfProxy)
|
|
666
|
+
// 3. If invoke returned a promise, await it then call target
|
|
667
|
+
if (transformedArgs && typeof transformedArgs.then === 'function') {
|
|
668
|
+
return transformedArgs.then((resolved: any) => target(resolved))
|
|
669
|
+
}
|
|
670
|
+
// 4. Call target with the transformed args
|
|
671
|
+
return target(transformedArgs)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
559
675
|
protected mapSetterFunctionProperty({ property }: { property: any }) {
|
|
560
676
|
const apiTarget = this.getApiTarget({ property })
|
|
561
677
|
const setterFn = property.definition.value
|
|
@@ -606,6 +722,12 @@ export class ContractCapsuleInstanceFactory {
|
|
|
606
722
|
this.disposeFunctions.push(boundFunction)
|
|
607
723
|
}
|
|
608
724
|
|
|
725
|
+
protected mapOnFreezeProperty({ property }: { property: any }) {
|
|
726
|
+
const selfProxy = this.createSelfProxy()
|
|
727
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
728
|
+
this.onFreezeFunctions.push(boundFunction)
|
|
729
|
+
}
|
|
730
|
+
|
|
609
731
|
async freeze(options: any): Promise<any> {
|
|
610
732
|
return this.freezeCapsule?.(options) || {}
|
|
611
733
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { join, dirname, resolve as pathResolve } from 'path'
|
|
1
|
+
import { join, dirname, relative, resolve as pathResolve } from 'path'
|
|
2
2
|
import { writeFile, mkdir, readFile, stat } from 'fs/promises'
|
|
3
3
|
import { Spine, SpineRuntime, CapsulePropertyTypes, makeImportStack, merge } from "../encapsulate"
|
|
4
|
-
import { StaticAnalyzer } from "../../src/static-analyzer
|
|
5
|
-
import { CapsuleModuleProjector } from "../../src/capsule-projectors/CapsuleModuleProjector
|
|
4
|
+
import { StaticAnalyzer } from "../../src/static-analyzer"
|
|
5
|
+
import { CapsuleModuleProjector } from "../../src/capsule-projectors/CapsuleModuleProjector"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
export { merge }
|
|
@@ -129,10 +129,39 @@ async function resolve(uri: string, fromPath: string, spineRoot?: string): Promi
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// Also traverse up from fromPath (the importing file) checking:
|
|
132
|
-
// 1.
|
|
133
|
-
// 2.
|
|
132
|
+
// 1. The scope/packages/pkg filesystem convention at each level
|
|
133
|
+
// 2. If current dir IS the package (self-package resolution)
|
|
134
|
+
// 3. node_modules/@scope/pkg at each level
|
|
134
135
|
let fromDir = dirname(fromPath)
|
|
135
136
|
while (true) {
|
|
137
|
+
// Check scope/packages/pkg filesystem convention
|
|
138
|
+
if (subpath) {
|
|
139
|
+
const fsPath = join(fromDir, scope, 'packages', pkg, subpath + '.ts')
|
|
140
|
+
try { await stat(fsPath); return fsPath } catch { }
|
|
141
|
+
try { await stat(join(fromDir, scope, 'packages', pkg, subpath)); return join(fromDir, scope, 'packages', pkg, subpath) } catch { }
|
|
142
|
+
}
|
|
143
|
+
const fsPkgDir = join(fromDir, scope, 'packages', pkg)
|
|
144
|
+
try {
|
|
145
|
+
const fsPjPath = join(fsPkgDir, 'package.json')
|
|
146
|
+
const fsPj = JSON.parse(await readFile(fsPjPath, 'utf-8'))
|
|
147
|
+
if (fsPj.name === `@${scope}/${pkg}`) {
|
|
148
|
+
if (subpath && fsPj.exports) {
|
|
149
|
+
const exportKey = './' + subpath
|
|
150
|
+
const exportValue = fsPj.exports[exportKey]
|
|
151
|
+
if (typeof exportValue === 'string') {
|
|
152
|
+
return pathResolve(fsPkgDir, exportValue)
|
|
153
|
+
}
|
|
154
|
+
} else if (!subpath && fsPj.exports?.['.']) {
|
|
155
|
+
const mainExport = fsPj.exports['.']
|
|
156
|
+
if (typeof mainExport === 'string') {
|
|
157
|
+
return pathResolve(fsPkgDir, mainExport)
|
|
158
|
+
}
|
|
159
|
+
} else if (!subpath && fsPj.main) {
|
|
160
|
+
return pathResolve(fsPkgDir, fsPj.main)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch { }
|
|
164
|
+
|
|
136
165
|
// Check if this directory's package.json matches the requested package
|
|
137
166
|
try {
|
|
138
167
|
const pjPath = join(fromDir, 'package.json')
|
|
@@ -278,6 +307,65 @@ async function resolve(uri: string, fromPath: string, spineRoot?: string): Promi
|
|
|
278
307
|
}
|
|
279
308
|
|
|
280
309
|
|
|
310
|
+
// Build an async npmUriForFilepath resolver backed by a directory→package-name cache.
|
|
311
|
+
// Walks up the directory tree from a given filepath to find the nearest package.json
|
|
312
|
+
// and constructs an npm-style URI (e.g. "package-name/src/foo.ts").
|
|
313
|
+
function createNpmUriForFilepath(): (filepath: string) => Promise<string | null> {
|
|
314
|
+
const cache = new Map<string, string | null>()
|
|
315
|
+
|
|
316
|
+
return async (absoluteFilepath: string): Promise<string | null> => {
|
|
317
|
+
// Only process absolute paths — skip V8 internal markers like "native", "node:*", etc.
|
|
318
|
+
if (!absoluteFilepath.startsWith('/')) {
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check for /node_modules/ in the path — use the last occurrence to handle nested node_modules
|
|
323
|
+
const nodeModulesMarker = '/node_modules/'
|
|
324
|
+
const lastIdx = absoluteFilepath.lastIndexOf(nodeModulesMarker)
|
|
325
|
+
if (lastIdx !== -1) {
|
|
326
|
+
return absoluteFilepath.substring(lastIdx + nodeModulesMarker.length)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let currentDir = dirname(absoluteFilepath)
|
|
330
|
+
const maxDepth = 20
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
333
|
+
if (cache.has(currentDir)) {
|
|
334
|
+
const cachedName = cache.get(currentDir)
|
|
335
|
+
if (cachedName) {
|
|
336
|
+
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
337
|
+
return `${cachedName}/${relativeFromPackage}`
|
|
338
|
+
}
|
|
339
|
+
// null means no package.json with name found at this level, continue up
|
|
340
|
+
const parentDir = dirname(currentDir)
|
|
341
|
+
if (parentDir === currentDir) break
|
|
342
|
+
currentDir = parentDir
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const packageJsonPath = join(currentDir, 'package.json')
|
|
347
|
+
try {
|
|
348
|
+
await stat(packageJsonPath)
|
|
349
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
|
|
350
|
+
const packageName = packageJson.name
|
|
351
|
+
cache.set(currentDir, packageName || null)
|
|
352
|
+
if (packageName) {
|
|
353
|
+
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
354
|
+
return `${packageName}/${relativeFromPackage}`
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
cache.set(currentDir, null)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const parentDir = dirname(currentDir)
|
|
361
|
+
if (parentDir === currentDir) break
|
|
362
|
+
currentDir = parentDir
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return null
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
281
369
|
export async function CapsuleSpineFactory({
|
|
282
370
|
spineFilesystemRoot,
|
|
283
371
|
capsuleModuleProjectionRoot,
|
|
@@ -329,8 +417,10 @@ export async function CapsuleSpineFactory({
|
|
|
329
417
|
}
|
|
330
418
|
|
|
331
419
|
const sourceSpine: { encapsulate?: any } = {}
|
|
420
|
+
const npmUriForFilepath = createNpmUriForFilepath()
|
|
332
421
|
const commonSpineContractOpts = {
|
|
333
422
|
spineFilesystemRoot,
|
|
423
|
+
npmUriForFilepath,
|
|
334
424
|
resolve: async (uri: string, parentFilepath: string) => {
|
|
335
425
|
// For relative paths, join with parent directory first
|
|
336
426
|
if (/^\.\.?\//.test(uri)) {
|
|
@@ -537,7 +627,28 @@ export async function CapsuleSpineFactory({
|
|
|
537
627
|
},
|
|
538
628
|
},
|
|
539
629
|
}) : undefined,
|
|
540
|
-
spineContracts: spineContractInstances.encapsulation
|
|
630
|
+
spineContracts: spineContractInstances.encapsulation,
|
|
631
|
+
projectionContext: capsuleModuleProjectionRoot ? {
|
|
632
|
+
capsuleModuleProjectionPackage,
|
|
633
|
+
capsuleModuleProjectionRoot,
|
|
634
|
+
projectionStore: {
|
|
635
|
+
writeFile: async (filepath: string, content: string) => {
|
|
636
|
+
filepath = join(capsuleModuleProjectionRoot, filepath)
|
|
637
|
+
await mkdir(dirname(filepath), { recursive: true })
|
|
638
|
+
await writeFile(filepath, content, 'utf-8')
|
|
639
|
+
},
|
|
640
|
+
getStats: async (filepath: string) => {
|
|
641
|
+
filepath = join(capsuleModuleProjectionRoot, filepath)
|
|
642
|
+
try {
|
|
643
|
+
const stats = await stat(filepath)
|
|
644
|
+
return { mtime: stats.mtime }
|
|
645
|
+
} catch (error) {
|
|
646
|
+
return null
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
get capsules() { return capsules }
|
|
651
|
+
} : undefined
|
|
541
652
|
})
|
|
542
653
|
sourceSpine.encapsulate = encapsulate
|
|
543
654
|
|
|
@@ -85,6 +85,7 @@ const MODULE_GLOBAL_BUILTINS = new Set([
|
|
|
85
85
|
|
|
86
86
|
// Bun runtime
|
|
87
87
|
'Bun',
|
|
88
|
+
'HTMLRewriter',
|
|
88
89
|
|
|
89
90
|
// Node.js Buffer
|
|
90
91
|
'Buffer',
|
|
@@ -218,6 +219,7 @@ const MODULE_GLOBAL_BUILTINS = new Set([
|
|
|
218
219
|
'decodeURIComponent',
|
|
219
220
|
'escape',
|
|
220
221
|
'unescape',
|
|
222
|
+
'eval',
|
|
221
223
|
])
|
|
222
224
|
|
|
223
225
|
export function StaticAnalyzer({
|
|
@@ -1939,6 +1941,19 @@ function extractAndValidateAmbientReferences(
|
|
|
1939
1941
|
extractBindingIdentifiers(param.name)
|
|
1940
1942
|
}
|
|
1941
1943
|
|
|
1944
|
+
// Pre-collect all function declarations from the function body (hoisting)
|
|
1945
|
+
// Function declarations are hoisted in JavaScript, so they can be referenced
|
|
1946
|
+
// before their source position. We must collect them before the main visit pass.
|
|
1947
|
+
function preCollectDeclarations(node: ts.Node) {
|
|
1948
|
+
if (ts.isFunctionDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
|
|
1949
|
+
localIdentifiers.add(node.name.text)
|
|
1950
|
+
}
|
|
1951
|
+
ts.forEachChild(node, preCollectDeclarations)
|
|
1952
|
+
}
|
|
1953
|
+
if (fn.body) {
|
|
1954
|
+
preCollectDeclarations(fn.body)
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1942
1957
|
// Traverse the function body to find identifiers
|
|
1943
1958
|
function visit(node: ts.Node) {
|
|
1944
1959
|
// Skip type nodes to avoid false positives from type annotations
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
export async function capsule({
|
|
3
|
+
encapsulate,
|
|
4
|
+
CapsulePropertyTypes,
|
|
5
|
+
makeImportStack
|
|
6
|
+
}: any) {
|
|
7
|
+
return encapsulate({
|
|
8
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
9
|
+
'#': {
|
|
10
|
+
// The parent capsule's full CST (source, spineContracts, ambient references, etc.)
|
|
11
|
+
parentCapsuleCst: {
|
|
12
|
+
type: CapsulePropertyTypes.Literal,
|
|
13
|
+
value: undefined
|
|
14
|
+
},
|
|
15
|
+
// The parent capsule's capsuleSourceLineRef
|
|
16
|
+
parentCapsuleSourceLineRef: {
|
|
17
|
+
type: CapsulePropertyTypes.Literal,
|
|
18
|
+
value: undefined
|
|
19
|
+
},
|
|
20
|
+
// Package prefix for projected capsule imports (e.g. '~caps')
|
|
21
|
+
capsuleModuleProjectionPackage: {
|
|
22
|
+
type: CapsulePropertyTypes.Literal,
|
|
23
|
+
value: undefined
|
|
24
|
+
},
|
|
25
|
+
// Store for writing projected files
|
|
26
|
+
projectionStore: {
|
|
27
|
+
type: CapsulePropertyTypes.Literal,
|
|
28
|
+
value: undefined
|
|
29
|
+
},
|
|
30
|
+
// All capsule snapshots for dependency resolution
|
|
31
|
+
capsuleSnapshots: {
|
|
32
|
+
type: CapsulePropertyTypes.Literal,
|
|
33
|
+
value: undefined
|
|
34
|
+
},
|
|
35
|
+
// The property contract delegate alias (e.g. '/apps/web/src/components/Counter1.tsx')
|
|
36
|
+
projectionPath: {
|
|
37
|
+
type: CapsulePropertyTypes.Literal,
|
|
38
|
+
value: undefined
|
|
39
|
+
},
|
|
40
|
+
// Spine contract URI
|
|
41
|
+
spineContractUri: {
|
|
42
|
+
type: CapsulePropertyTypes.Literal,
|
|
43
|
+
value: undefined
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, {
|
|
48
|
+
importMeta: import.meta,
|
|
49
|
+
importStack: makeImportStack(),
|
|
50
|
+
capsuleName: capsule['#'],
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
capsule['#'] = '@stream44.studio/encapsulate/structs/CapsuleProjectionContext'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
|
|
2
|
+
export async function capsule({
|
|
3
|
+
encapsulate,
|
|
4
|
+
CapsulePropertyTypes,
|
|
5
|
+
makeImportStack
|
|
6
|
+
}: any) {
|
|
7
|
+
return encapsulate({
|
|
8
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
9
|
+
'#': {
|
|
10
|
+
// --------------------------------------------------------
|
|
11
|
+
// _instances — internal store for keyed instances
|
|
12
|
+
// --------------------------------------------------------
|
|
13
|
+
_instances: {
|
|
14
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
15
|
+
value: function () {
|
|
16
|
+
return new Map<string, any>();
|
|
17
|
+
},
|
|
18
|
+
memoize: true,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// --------------------------------------------------------
|
|
22
|
+
// _defaults — override in extending structs to provide
|
|
23
|
+
// default config for each new instance
|
|
24
|
+
// --------------------------------------------------------
|
|
25
|
+
_defaults: {
|
|
26
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
27
|
+
value: function () {
|
|
28
|
+
return {};
|
|
29
|
+
},
|
|
30
|
+
memoize: true,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// --------------------------------------------------------
|
|
34
|
+
// config — user-provided overrides applied to all instances
|
|
35
|
+
// --------------------------------------------------------
|
|
36
|
+
config: {
|
|
37
|
+
type: CapsulePropertyTypes.Literal,
|
|
38
|
+
value: {} as any,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// --------------------------------------------------------
|
|
42
|
+
// forInstance — get-or-create a config instance by key
|
|
43
|
+
// Pass an object whose keys together form a composite key.
|
|
44
|
+
// Returns a merged config (defaults + factory config + instance overrides).
|
|
45
|
+
// --------------------------------------------------------
|
|
46
|
+
forInstance: {
|
|
47
|
+
type: CapsulePropertyTypes.Function,
|
|
48
|
+
value: function (this: any, keyObj: Record<string, any>, instanceConfig?: Record<string, any>) {
|
|
49
|
+
const serializedKey = JSON.stringify(
|
|
50
|
+
Object.keys(keyObj).sort().reduce((acc: any, k: string) => {
|
|
51
|
+
acc[k] = keyObj[k];
|
|
52
|
+
return acc;
|
|
53
|
+
}, {} as any)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (!instanceConfig && this._instances.has(serializedKey)) {
|
|
57
|
+
return this._instances.get(serializedKey);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const merged = {
|
|
61
|
+
...this._defaults,
|
|
62
|
+
...this.config,
|
|
63
|
+
...keyObj,
|
|
64
|
+
...(instanceConfig || {}),
|
|
65
|
+
...(this._instances.get(serializedKey) || {}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this._instances.set(serializedKey, merged);
|
|
69
|
+
return merged;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// --------------------------------------------------------
|
|
74
|
+
// getInstances — return all tracked instances
|
|
75
|
+
// --------------------------------------------------------
|
|
76
|
+
getInstances: {
|
|
77
|
+
type: CapsulePropertyTypes.Function,
|
|
78
|
+
value: function (this: any) {
|
|
79
|
+
return Array.from(this._instances.values());
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, {
|
|
85
|
+
importMeta: import.meta,
|
|
86
|
+
importStack: makeImportStack(),
|
|
87
|
+
capsuleName: capsule['#'],
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
capsule['#'] = '@stream44.studio/encapsulate/structs/StructFactory'
|