@stream44.studio/encapsulate 0.4.0-rc.22 → 0.4.0-rc.24
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 -1
- package/package.json +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +20 -3
- package/src/encapsulate.ts +58 -5
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +99 -74
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +46 -4
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +127 -1
- package/src/static-analyzer.v0.ts +110 -11
- package/tsconfig.json +1 -2
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<td><a href="https://Stream44.Studio"><img src=".o/stream44.studio/assets/Icon-v1.svg" width="42" height="42"></a></td>
|
|
4
4
|
<td><strong><a href="https://Stream44.Studio">Stream44 Studio</a></strong><br/>Open Development Project</td>
|
|
5
5
|
<td>Preview release for community feedback.<br/>Get in touch on <a href="https://discord.gg/9eBcQXEJAN">discord</a>.</td>
|
|
6
|
+
<td>Hand Designed<br/><b>AI Coded Alpha</a></td>
|
|
6
7
|
</tr>
|
|
7
8
|
</table>
|
|
8
9
|
|
|
@@ -16,6 +17,7 @@ An *experimental* implementation of the [PrivateData.Space](https://privatedata.
|
|
|
16
17
|
***NOTE:** Not intended for direct use until it matures in light of the projects below.*
|
|
17
18
|
|
|
18
19
|
It is being used to underpin:
|
|
20
|
+
- [Framespace Genesis](https://github.com/Stream44/FramespaceGenesis) - Modeling engine with realtime interactive visualization
|
|
19
21
|
- [t44](https://github.com/Stream44/t44) - A web3 + AI ready workspace
|
|
20
22
|
- [Stream44.Studio](https://stream44.studio) - A **full-stack IDE** for building **embodied distributed systems**
|
|
21
23
|
|
|
@@ -44,7 +46,6 @@ The capsule spine contract is implemented here: [src/spine-contracts/CapsuleSpin
|
|
|
44
46
|
|
|
45
47
|

|
|
46
48
|
|
|
47
|
-
|
|
48
49
|
Provenance
|
|
49
50
|
===
|
|
50
51
|
|
package/package.json
CHANGED
|
@@ -28,8 +28,11 @@ function safeCapsuleName(name: string) {
|
|
|
28
28
|
* @returns The cache file path to use
|
|
29
29
|
*/
|
|
30
30
|
async function constructCacheFilePath(moduleFilepath: string, importStackLine: number, spineFilesystemRoot: string): Promise<string> {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const isExternal = moduleFilepath.startsWith('../')
|
|
32
|
+
const hasNodeModules = moduleFilepath.includes('node_modules/')
|
|
33
|
+
|
|
34
|
+
if (isExternal || hasNodeModules) {
|
|
35
|
+
// External module or node_modules path - construct npm URI
|
|
33
36
|
const absoluteFilepath = join(spineFilesystemRoot, moduleFilepath)
|
|
34
37
|
const npmUri = await constructNpmUriForCache(absoluteFilepath, spineFilesystemRoot)
|
|
35
38
|
if (npmUri) {
|
|
@@ -100,6 +103,7 @@ export function CapsuleModuleProjector({
|
|
|
100
103
|
projectionStore,
|
|
101
104
|
projectionCacheStore,
|
|
102
105
|
spineFilesystemRoot,
|
|
106
|
+
capsuleModuleProjectionRoot,
|
|
103
107
|
capsuleModuleProjectionPackage,
|
|
104
108
|
timing
|
|
105
109
|
}: {
|
|
@@ -117,6 +121,7 @@ export function CapsuleModuleProjector({
|
|
|
117
121
|
getStats?: (filepath: string) => Promise<{ mtime: Date } | null>
|
|
118
122
|
},
|
|
119
123
|
spineFilesystemRoot: string,
|
|
124
|
+
capsuleModuleProjectionRoot?: string,
|
|
120
125
|
capsuleModuleProjectionPackage?: string,
|
|
121
126
|
timing?: { record: (step: string) => void, chalk?: any }
|
|
122
127
|
}) {
|
|
@@ -736,6 +741,18 @@ export function CapsuleModuleProjector({
|
|
|
736
741
|
delete ambientReferences['makeImportStack']
|
|
737
742
|
}
|
|
738
743
|
|
|
744
|
+
// Compute the relative path from the projected caps file directory
|
|
745
|
+
// back to the original source file directory so that relative imports
|
|
746
|
+
// in the caps file resolve to the original source location.
|
|
747
|
+
const absSourceDir = dirname(join(spineFilesystemRoot, capsule.cst.source.moduleFilepath))
|
|
748
|
+
const projectionRoot = capsuleModuleProjectionRoot || spineFilesystemRoot
|
|
749
|
+
const absCapsDir = dirname(join(projectionRoot, filepath))
|
|
750
|
+
const capsToSourcePrefix = relative(absCapsDir, absSourceDir)
|
|
751
|
+
function rewriteRelativeModuleUri(moduleUri: string): string {
|
|
752
|
+
if (!moduleUri.startsWith('./') && !moduleUri.startsWith('../')) return moduleUri
|
|
753
|
+
return capsToSourcePrefix + '/' + moduleUri.replace(/^\.\//, '')
|
|
754
|
+
}
|
|
755
|
+
|
|
739
756
|
const importStatements = Object.entries(ambientReferences)
|
|
740
757
|
.map(([name, ref]: [string, any]) => {
|
|
741
758
|
if (ref.type === 'import') {
|
|
@@ -745,7 +762,7 @@ export function CapsuleModuleProjector({
|
|
|
745
762
|
const cssPath = cssImportMapping[ref.moduleUri] || ref.moduleUri
|
|
746
763
|
return `import '${cssPath}'`
|
|
747
764
|
}
|
|
748
|
-
return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
|
|
765
|
+
return `import ${ref.importSpecifier} from '${rewriteRelativeModuleUri(ref.moduleUri)}'`
|
|
749
766
|
}
|
|
750
767
|
if (ref.type === 'assigned') {
|
|
751
768
|
// If the assignment comes from a spine-factory module, import from encapsulate.ts instead
|
package/src/encapsulate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
// CACHE_BUST_VERSION: Increment this whenever CST cache must be invalidated due to structural changes
|
|
3
3
|
// This ensures projected capsules are regenerated when the CST format changes
|
|
4
|
-
const CACHE_BUST_VERSION =
|
|
4
|
+
const CACHE_BUST_VERSION = 21
|
|
5
5
|
|
|
6
6
|
type TSpineOptions = {
|
|
7
7
|
spineFilesystemRoot?: string,
|
|
@@ -53,7 +53,9 @@ type TCapsuleMakeInstanceOptions = {
|
|
|
53
53
|
capsuleName: string,
|
|
54
54
|
capsuleSourceLineRef: string,
|
|
55
55
|
moduleFilepath: string
|
|
56
|
-
}
|
|
56
|
+
},
|
|
57
|
+
parentCapsuleSourceUriLineRefInstanceId?: string,
|
|
58
|
+
sit?: { capsuleInstances: Record<string, { capsuleName: string, capsuleSourceUriLineRef: string, parentCapsuleSourceUriLineRefInstanceId: string }> }
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
type TCapsule = {
|
|
@@ -552,7 +554,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
552
554
|
encapsulateOptions,
|
|
553
555
|
cst,
|
|
554
556
|
crt: crts?.[capsuleSourceLineRef],
|
|
555
|
-
makeInstance: async ({ overrides = {}, options = {}, runtimeSpineContracts, sharedSelf, rootCapsule }: TCapsuleMakeInstanceOptions = {}) => {
|
|
557
|
+
makeInstance: async ({ overrides = {}, options = {}, runtimeSpineContracts, sharedSelf, rootCapsule, parentCapsuleSourceUriLineRefInstanceId, sit }: TCapsuleMakeInstanceOptions = {}) => {
|
|
556
558
|
|
|
557
559
|
// Create cache key based on parameters
|
|
558
560
|
// When sharedSelf is provided, we must NOT cache because each extending capsule
|
|
@@ -811,8 +813,27 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
811
813
|
capsuleName: encapsulateOptions.capsuleName!,
|
|
812
814
|
capsuleSourceLineRef: absoluteCapsuleSourceLineRef,
|
|
813
815
|
moduleFilepath: absoluteModuleFilepath
|
|
814
|
-
}
|
|
816
|
+
},
|
|
817
|
+
parentCapsuleSourceUriLineRefInstanceId: parentCapsuleSourceUriLineRefInstanceId
|
|
818
|
+
? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + (cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef))
|
|
819
|
+
: sha256(cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef),
|
|
820
|
+
sit
|
|
815
821
|
})
|
|
822
|
+
|
|
823
|
+
// Propagate this (child) capsule's encapsulatedApi to all parent spine contract
|
|
824
|
+
// instances up the extends chain, so parent functions can resolve child Function
|
|
825
|
+
// properties via this (e.g. Engine.mergeNode calling this._mergeNode from QueryAPI).
|
|
826
|
+
// Each ancestor accumulates child APIs so the proxy can check all levels.
|
|
827
|
+
let ancestor = extendedCapsuleInstance
|
|
828
|
+
while (ancestor) {
|
|
829
|
+
for (const sci of Object.values(ancestor.spineContractCapsuleInstances || {})) {
|
|
830
|
+
if (!(sci as any).childEncapsulatedApis) {
|
|
831
|
+
(sci as any).childEncapsulatedApis = []
|
|
832
|
+
}
|
|
833
|
+
; (sci as any).childEncapsulatedApis.push(encapsulatedApi)
|
|
834
|
+
}
|
|
835
|
+
ancestor = ancestor.extendedCapsuleInstance
|
|
836
|
+
}
|
|
816
837
|
}
|
|
817
838
|
|
|
818
839
|
// Resolve the root capsule for this instance:
|
|
@@ -827,6 +848,23 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
827
848
|
capsuleMetadataStruct.rootCapsule.capsuleSourceLineRef = resolvedRootCapsule.capsuleSourceLineRef
|
|
828
849
|
capsuleMetadataStruct.rootCapsule.moduleFilepath = resolvedRootCapsule.moduleFilepath
|
|
829
850
|
|
|
851
|
+
// Compute deterministic instance ID:
|
|
852
|
+
// root: sha256(capsuleSourceUriLineRef)
|
|
853
|
+
// child: sha256(parentCapsuleSourceUriLineRefInstanceId + ":" + capsuleSourceUriLineRef)
|
|
854
|
+
const capsuleSourceUriLineRef = cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef
|
|
855
|
+
const capsuleSourceUriLineRefInstanceId = parentCapsuleSourceUriLineRefInstanceId
|
|
856
|
+
? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + capsuleSourceUriLineRef)
|
|
857
|
+
: sha256(capsuleSourceUriLineRef)
|
|
858
|
+
|
|
859
|
+
// Register this instance in the sit structure if provided
|
|
860
|
+
if (sit) {
|
|
861
|
+
sit.capsuleInstances[capsuleSourceUriLineRefInstanceId] = {
|
|
862
|
+
capsuleName: encapsulateOptions.capsuleName || '',
|
|
863
|
+
capsuleSourceUriLineRef,
|
|
864
|
+
parentCapsuleSourceUriLineRefInstanceId: parentCapsuleSourceUriLineRefInstanceId || ''
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
830
868
|
const capsuleInstance: any = {
|
|
831
869
|
api: encapsulatedApi,
|
|
832
870
|
spineContractCapsuleInstances,
|
|
@@ -836,7 +874,11 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
836
874
|
initFunctions: [] as Array<() => any>,
|
|
837
875
|
disposeFunctions: [] as Array<() => any>,
|
|
838
876
|
mappedCapsuleInstances: [] as Array<any>,
|
|
839
|
-
rootCapsule: resolvedRootCapsule
|
|
877
|
+
rootCapsule: resolvedRootCapsule,
|
|
878
|
+
capsuleSourceUriLineRefInstanceId,
|
|
879
|
+
capsuleName: encapsulateOptions.capsuleName,
|
|
880
|
+
capsuleSourceUriLineRef,
|
|
881
|
+
sit
|
|
840
882
|
}
|
|
841
883
|
|
|
842
884
|
// Set capsule metadata struct on self early so it's available in options() callbacks during mapping
|
|
@@ -1062,6 +1104,17 @@ function relative(from: string, to: string): string {
|
|
|
1062
1104
|
return result || '.'
|
|
1063
1105
|
}
|
|
1064
1106
|
|
|
1107
|
+
function sha256(input: string): string {
|
|
1108
|
+
// Use Bun's native hasher for speed; falls back to Node crypto
|
|
1109
|
+
if (typeof globalThis.Bun !== 'undefined') {
|
|
1110
|
+
const hasher = new globalThis.Bun.CryptoHasher('sha256')
|
|
1111
|
+
hasher.update(input)
|
|
1112
|
+
return hasher.digest('hex') as string
|
|
1113
|
+
}
|
|
1114
|
+
const { createHash } = require('crypto')
|
|
1115
|
+
return createHash('sha256').update(input).digest('hex')
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1065
1118
|
function isObject(item: any): boolean {
|
|
1066
1119
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return false
|
|
1067
1120
|
// Only deep-merge plain objects — preserve instances like Map, Set, Date, etc.
|
|
@@ -6,6 +6,7 @@ type CallerContext = {
|
|
|
6
6
|
capsuleSourceNameRef?: string
|
|
7
7
|
spineContractCapsuleInstanceId: string
|
|
8
8
|
capsuleSourceNameRefHash?: string
|
|
9
|
+
capsuleSourceUriLineRefInstanceId?: string
|
|
9
10
|
prop?: string
|
|
10
11
|
filepath?: string
|
|
11
12
|
line?: number
|
|
@@ -53,7 +54,8 @@ function CapsuleMembrane(target: Record<string, any>, hooks?: {
|
|
|
53
54
|
class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFactory {
|
|
54
55
|
private getEventIndex: () => number
|
|
55
56
|
private incrementEventIndex: () => number
|
|
56
|
-
private
|
|
57
|
+
private getCurrentCallerContext: () => CallerContext | undefined
|
|
58
|
+
private setCurrentCallerContext: (ctx: CallerContext | undefined) => void
|
|
57
59
|
private onMembraneEvent?: (event: any) => void
|
|
58
60
|
private enableCallerStackInference: boolean
|
|
59
61
|
private encapsulateOptions: any
|
|
@@ -77,7 +79,8 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
77
79
|
encapsulateOptions,
|
|
78
80
|
getEventIndex,
|
|
79
81
|
incrementEventIndex,
|
|
80
|
-
|
|
82
|
+
getCurrentCallerContext,
|
|
83
|
+
setCurrentCallerContext,
|
|
81
84
|
runtimeSpineContracts,
|
|
82
85
|
instanceRegistry,
|
|
83
86
|
extendedCapsuleInstance,
|
|
@@ -97,7 +100,8 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
97
100
|
encapsulateOptions: any
|
|
98
101
|
getEventIndex: () => number
|
|
99
102
|
incrementEventIndex: () => number
|
|
100
|
-
|
|
103
|
+
getCurrentCallerContext: () => CallerContext | undefined
|
|
104
|
+
setCurrentCallerContext: (ctx: CallerContext | undefined) => void
|
|
101
105
|
runtimeSpineContracts?: Record<string, any>
|
|
102
106
|
instanceRegistry?: CapsuleInstanceRegistry
|
|
103
107
|
extendedCapsuleInstance?: any
|
|
@@ -106,7 +110,8 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
106
110
|
super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, capsuleInstance })
|
|
107
111
|
this.getEventIndex = getEventIndex
|
|
108
112
|
this.incrementEventIndex = incrementEventIndex
|
|
109
|
-
this.
|
|
113
|
+
this.getCurrentCallerContext = getCurrentCallerContext
|
|
114
|
+
this.setCurrentCallerContext = setCurrentCallerContext
|
|
110
115
|
this.onMembraneEvent = onMembraneEvent
|
|
111
116
|
this.enableCallerStackInference = enableCallerStackInference
|
|
112
117
|
this.encapsulateOptions = encapsulateOptions
|
|
@@ -116,8 +121,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
116
121
|
this.id = `$${encapsulateOptions.capsuleSourceLineRef}`
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
private buildCallerContext(prop?: string): CallerContext {
|
|
125
|
+
const ctx: CallerContext = {
|
|
126
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
127
|
+
spineContractCapsuleInstanceId: this.id,
|
|
128
|
+
}
|
|
129
|
+
if (prop) ctx.prop = prop
|
|
130
|
+
if (this.capsuleSourceNameRef) ctx.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
131
|
+
if (this.capsuleSourceNameRefHash) ctx.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
132
|
+
if (this.capsuleInstance?.capsuleSourceUriLineRefInstanceId) ctx.capsuleSourceUriLineRefInstanceId = this.capsuleInstance.capsuleSourceUriLineRefInstanceId
|
|
133
|
+
return ctx
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
protected async mapMappingProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
|
|
@@ -155,25 +168,23 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
155
168
|
throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.currentCallerContext.filepath = callerInfo.filepath
|
|
173
|
-
this.currentCallerContext.line = callerInfo.line
|
|
174
|
-
this.currentCallerContext.stack = stackFrames
|
|
171
|
+
// Only update caller context if not already set by a function/getter execution
|
|
172
|
+
if (!this.getCurrentCallerContext()) {
|
|
173
|
+
const callerCtx = this.buildCallerContext(undefined)
|
|
174
|
+
|
|
175
|
+
if (this.enableCallerStackInference) {
|
|
176
|
+
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
177
|
+
if (stackStr) {
|
|
178
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
179
|
+
if (stackFrames.length > 0) {
|
|
180
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
181
|
+
callerCtx.filepath = callerInfo.filepath
|
|
182
|
+
callerCtx.line = callerInfo.line
|
|
183
|
+
callerCtx.stack = stackFrames
|
|
184
|
+
}
|
|
175
185
|
}
|
|
176
186
|
}
|
|
187
|
+
this.setCurrentCallerContext(callerCtx)
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
// Access through .api if it exists (for capsule instances with getters)
|
|
@@ -280,25 +291,23 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
280
291
|
get: (apiTarget: any, apiProp: string | symbol) => {
|
|
281
292
|
if (typeof apiProp === 'symbol') return apiTarget[apiProp]
|
|
282
293
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.currentCallerContext.filepath = callerInfo.filepath
|
|
298
|
-
this.currentCallerContext.line = callerInfo.line
|
|
299
|
-
this.currentCallerContext.stack = stackFrames
|
|
294
|
+
// Only update caller context if not already set by a function/getter execution
|
|
295
|
+
if (!this.getCurrentCallerContext()) {
|
|
296
|
+
const callerCtx = this.buildCallerContext(undefined)
|
|
297
|
+
|
|
298
|
+
if (this.enableCallerStackInference) {
|
|
299
|
+
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
300
|
+
if (stackStr) {
|
|
301
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
302
|
+
if (stackFrames.length > 0) {
|
|
303
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
304
|
+
callerCtx.filepath = callerInfo.filepath
|
|
305
|
+
callerCtx.line = callerInfo.line
|
|
306
|
+
callerCtx.stack = stackFrames
|
|
307
|
+
}
|
|
300
308
|
}
|
|
301
309
|
}
|
|
310
|
+
this.setCurrentCallerContext(callerCtx)
|
|
302
311
|
}
|
|
303
312
|
|
|
304
313
|
// Access through .api if it exists (for capsule instances with getters)
|
|
@@ -345,25 +354,23 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
345
354
|
// Wrap the property access in a proxy to track membrane events
|
|
346
355
|
Object.defineProperty(delegateTarget, key, {
|
|
347
356
|
get: () => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
this.currentCallerContext.filepath = callerInfo.filepath
|
|
363
|
-
this.currentCallerContext.line = callerInfo.line
|
|
364
|
-
this.currentCallerContext.stack = stackFrames
|
|
357
|
+
// Only update caller context if not already set by a function/getter execution
|
|
358
|
+
if (!this.getCurrentCallerContext()) {
|
|
359
|
+
const callerCtx = this.buildCallerContext(undefined)
|
|
360
|
+
|
|
361
|
+
if (this.enableCallerStackInference) {
|
|
362
|
+
const stackStr = new Error('[PROPERTY_CONTRACT_DELEGATE]').stack
|
|
363
|
+
if (stackStr) {
|
|
364
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
365
|
+
if (stackFrames.length > 0) {
|
|
366
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
367
|
+
callerCtx.filepath = callerInfo.filepath
|
|
368
|
+
callerCtx.line = callerInfo.line
|
|
369
|
+
callerCtx.stack = stackFrames
|
|
370
|
+
}
|
|
365
371
|
}
|
|
366
372
|
}
|
|
373
|
+
this.setCurrentCallerContext(callerCtx)
|
|
367
374
|
}
|
|
368
375
|
|
|
369
376
|
// Access the actual value from the instance's api
|
|
@@ -513,6 +520,9 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
513
520
|
if (this.capsuleSourceNameRefHash) {
|
|
514
521
|
callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
515
522
|
}
|
|
523
|
+
if (this.capsuleInstance?.capsuleSourceUriLineRefInstanceId) {
|
|
524
|
+
callEvent.target.capsuleSourceUriLineRefInstanceId = this.capsuleInstance.capsuleSourceUriLineRefInstanceId
|
|
525
|
+
}
|
|
516
526
|
|
|
517
527
|
this.addCallerContextToEvent(callEvent)
|
|
518
528
|
this.onMembraneEvent?.(callEvent)
|
|
@@ -550,11 +560,18 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
550
560
|
if (this.capsuleSourceNameRefHash) {
|
|
551
561
|
callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
552
562
|
}
|
|
563
|
+
if (this.capsuleInstance?.capsuleSourceUriLineRefInstanceId) {
|
|
564
|
+
callEvent.target.capsuleSourceUriLineRefInstanceId = this.capsuleInstance.capsuleSourceUriLineRefInstanceId
|
|
565
|
+
}
|
|
553
566
|
|
|
554
567
|
this.addCallerContextToEvent(callEvent)
|
|
555
568
|
this.onMembraneEvent?.(callEvent)
|
|
556
569
|
|
|
570
|
+
// Set this capsule as caller for any inner membrane events triggered by the function body
|
|
571
|
+
const previousCallerContext = this.getCurrentCallerContext()
|
|
572
|
+
this.setCurrentCallerContext(this.buildCallerContext(property.name))
|
|
557
573
|
const result = boundFunction(...args)
|
|
574
|
+
this.setCurrentCallerContext(previousCallerContext)
|
|
558
575
|
|
|
559
576
|
// Store in memoize cache if memoize is enabled
|
|
560
577
|
if (shouldMemoize) {
|
|
@@ -635,8 +652,11 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
635
652
|
return cachedResult
|
|
636
653
|
}
|
|
637
654
|
|
|
638
|
-
//
|
|
655
|
+
// Set this capsule as caller for any inner membrane events triggered by the getter body
|
|
656
|
+
const previousCallerContext = this.getCurrentCallerContext()
|
|
657
|
+
this.setCurrentCallerContext(this.buildCallerContext(property.name))
|
|
639
658
|
const result = getterFn.call(selfProxy)
|
|
659
|
+
this.setCurrentCallerContext(previousCallerContext)
|
|
640
660
|
|
|
641
661
|
// Store in memoize cache if memoize is enabled
|
|
642
662
|
if (shouldMemoize) {
|
|
@@ -693,28 +713,32 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
693
713
|
}
|
|
694
714
|
|
|
695
715
|
private addCallerContextToEvent(event: any): void {
|
|
696
|
-
|
|
716
|
+
const callerCtx = this.getCurrentCallerContext()
|
|
717
|
+
if (callerCtx) {
|
|
697
718
|
event.caller = {
|
|
698
|
-
capsuleSourceLineRef:
|
|
699
|
-
spineContractCapsuleInstanceId:
|
|
719
|
+
capsuleSourceLineRef: callerCtx.capsuleSourceLineRef,
|
|
720
|
+
spineContractCapsuleInstanceId: callerCtx.spineContractCapsuleInstanceId,
|
|
721
|
+
}
|
|
722
|
+
if (callerCtx.capsuleSourceNameRef) {
|
|
723
|
+
event.caller.capsuleSourceNameRef = callerCtx.capsuleSourceNameRef
|
|
700
724
|
}
|
|
701
|
-
if (
|
|
702
|
-
event.caller.
|
|
725
|
+
if (callerCtx.capsuleSourceNameRefHash) {
|
|
726
|
+
event.caller.capsuleSourceNameRefHash = callerCtx.capsuleSourceNameRefHash
|
|
703
727
|
}
|
|
704
|
-
if (
|
|
705
|
-
event.caller.
|
|
728
|
+
if (callerCtx.capsuleSourceUriLineRefInstanceId) {
|
|
729
|
+
event.caller.capsuleSourceUriLineRefInstanceId = callerCtx.capsuleSourceUriLineRefInstanceId
|
|
706
730
|
}
|
|
707
|
-
if (
|
|
708
|
-
event.caller.prop =
|
|
731
|
+
if (callerCtx.prop) {
|
|
732
|
+
event.caller.prop = callerCtx.prop
|
|
709
733
|
}
|
|
710
|
-
if (
|
|
711
|
-
event.caller.filepath =
|
|
734
|
+
if (callerCtx.filepath) {
|
|
735
|
+
event.caller.filepath = callerCtx.filepath
|
|
712
736
|
}
|
|
713
|
-
if (
|
|
714
|
-
event.caller.line =
|
|
737
|
+
if (callerCtx.line) {
|
|
738
|
+
event.caller.line = callerCtx.line
|
|
715
739
|
}
|
|
716
|
-
if (
|
|
717
|
-
event.caller.stack =
|
|
740
|
+
if (callerCtx.stack) {
|
|
741
|
+
event.caller.stack = callerCtx.stack
|
|
718
742
|
}
|
|
719
743
|
} else if (this.enableCallerStackInference) {
|
|
720
744
|
const stackStr = new Error('[MEMBRANE_EVENT]').stack
|
|
@@ -771,7 +795,8 @@ export function CapsuleSpineContract({
|
|
|
771
795
|
encapsulateOptions,
|
|
772
796
|
getEventIndex: () => eventIndex,
|
|
773
797
|
incrementEventIndex: () => eventIndex++,
|
|
774
|
-
currentCallerContext,
|
|
798
|
+
getCurrentCallerContext: () => currentCallerContext,
|
|
799
|
+
setCurrentCallerContext: (ctx: CallerContext | undefined) => { currentCallerContext = ctx },
|
|
775
800
|
runtimeSpineContracts,
|
|
776
801
|
instanceRegistry,
|
|
777
802
|
extendedCapsuleInstance,
|
|
@@ -16,6 +16,7 @@ export class ContractCapsuleInstanceFactory {
|
|
|
16
16
|
protected instanceRegistry?: CapsuleInstanceRegistry
|
|
17
17
|
protected extendedCapsuleInstance?: any
|
|
18
18
|
protected ownSelf?: any
|
|
19
|
+
public childEncapsulatedApis?: Record<string, any>[]
|
|
19
20
|
protected runtimeSpineContracts?: Record<string, any>
|
|
20
21
|
protected capsuleInstance?: any
|
|
21
22
|
public structInitFunctions: Array<() => any> = []
|
|
@@ -61,7 +62,9 @@ export class ContractCapsuleInstanceFactory {
|
|
|
61
62
|
overrides: overrides || {},
|
|
62
63
|
options: options,
|
|
63
64
|
runtimeSpineContracts: this.runtimeSpineContracts,
|
|
64
|
-
rootCapsule: this.capsuleInstance?.rootCapsule
|
|
65
|
+
rootCapsule: this.capsuleInstance?.rootCapsule,
|
|
66
|
+
parentCapsuleSourceUriLineRefInstanceId: this.capsuleInstance?.capsuleSourceUriLineRefInstanceId,
|
|
67
|
+
sit: this.capsuleInstance?.sit
|
|
65
68
|
})
|
|
66
69
|
|
|
67
70
|
// Run init functions on the imported capsule instance
|
|
@@ -310,7 +313,9 @@ export class ContractCapsuleInstanceFactory {
|
|
|
310
313
|
overrides: mappedOverrides,
|
|
311
314
|
options: ownMappingOptions,
|
|
312
315
|
runtimeSpineContracts: this.runtimeSpineContracts,
|
|
313
|
-
rootCapsule: this.capsuleInstance?.rootCapsule
|
|
316
|
+
rootCapsule: this.capsuleInstance?.rootCapsule,
|
|
317
|
+
parentCapsuleSourceUriLineRefInstanceId: this.capsuleInstance?.capsuleSourceUriLineRefInstanceId,
|
|
318
|
+
sit: this.capsuleInstance?.sit
|
|
314
319
|
})
|
|
315
320
|
|
|
316
321
|
// Register the instance (replaces null pre-registration marker)
|
|
@@ -369,6 +374,7 @@ export class ContractCapsuleInstanceFactory {
|
|
|
369
374
|
protected createSelfProxy() {
|
|
370
375
|
const extendedApi = this.extendedCapsuleInstance?.api
|
|
371
376
|
const ownSelf = this.ownSelf
|
|
377
|
+
const factory = this
|
|
372
378
|
return new Proxy(this.self, {
|
|
373
379
|
get: (target: any, prop: string | symbol) => {
|
|
374
380
|
if (typeof prop === 'symbol') return target[prop]
|
|
@@ -384,8 +390,15 @@ export class ContractCapsuleInstanceFactory {
|
|
|
384
390
|
}
|
|
385
391
|
|
|
386
392
|
// Fall back to encapsulatedApi
|
|
387
|
-
if (prop in
|
|
388
|
-
return
|
|
393
|
+
if (prop in factory.encapsulatedApi) {
|
|
394
|
+
return factory.encapsulatedApi[prop]
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Fall back to child capsule APIs (for parent→child function delegation)
|
|
398
|
+
if (factory.childEncapsulatedApis) {
|
|
399
|
+
for (const childApi of factory.childEncapsulatedApis) {
|
|
400
|
+
if (prop in childApi) return childApi[prop]
|
|
401
|
+
}
|
|
389
402
|
}
|
|
390
403
|
|
|
391
404
|
// Fall back to extended capsule's API
|
|
@@ -393,6 +406,35 @@ export class ContractCapsuleInstanceFactory {
|
|
|
393
406
|
return extendedApi[prop]
|
|
394
407
|
}
|
|
395
408
|
|
|
409
|
+
return undefined
|
|
410
|
+
},
|
|
411
|
+
ownKeys: (target: any) => {
|
|
412
|
+
const keys = new Set<string>(Object.keys(target))
|
|
413
|
+
for (const k of Object.keys(factory.encapsulatedApi)) keys.add(k)
|
|
414
|
+
if (factory.childEncapsulatedApis) {
|
|
415
|
+
for (const childApi of factory.childEncapsulatedApis) {
|
|
416
|
+
for (const k of Object.keys(childApi)) keys.add(k)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (extendedApi) {
|
|
420
|
+
for (const k of Object.keys(extendedApi)) keys.add(k)
|
|
421
|
+
}
|
|
422
|
+
return [...keys]
|
|
423
|
+
},
|
|
424
|
+
set: (target: any, prop: string | symbol, value: any) => {
|
|
425
|
+
target[prop] = value
|
|
426
|
+
return true
|
|
427
|
+
},
|
|
428
|
+
getOwnPropertyDescriptor: (target: any, prop: string | symbol) => {
|
|
429
|
+
if (typeof prop === 'symbol') return Object.getOwnPropertyDescriptor(target, prop)
|
|
430
|
+
if (prop in target) return Object.getOwnPropertyDescriptor(target, prop)
|
|
431
|
+
if (prop in factory.encapsulatedApi) return { configurable: true, enumerable: true, writable: true, value: factory.encapsulatedApi[prop as string] }
|
|
432
|
+
if (factory.childEncapsulatedApis) {
|
|
433
|
+
for (const childApi of factory.childEncapsulatedApis) {
|
|
434
|
+
if (prop in childApi) return { configurable: true, enumerable: true, writable: true, value: childApi[prop as string] }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (extendedApi && prop in extendedApi) return { configurable: true, enumerable: true, writable: true, value: extendedApi[prop as string] }
|
|
396
438
|
return undefined
|
|
397
439
|
}
|
|
398
440
|
})
|
|
@@ -473,6 +473,7 @@ export async function CapsuleSpineFactory({
|
|
|
473
473
|
},
|
|
474
474
|
},
|
|
475
475
|
spineFilesystemRoot,
|
|
476
|
+
capsuleModuleProjectionRoot,
|
|
476
477
|
capsuleModuleProjectionPackage,
|
|
477
478
|
timing
|
|
478
479
|
}) : undefined
|
|
@@ -570,13 +571,138 @@ export async function CapsuleSpineFactory({
|
|
|
570
571
|
return capsule
|
|
571
572
|
}
|
|
572
573
|
|
|
574
|
+
// Wrap freeze to also write spine instance (.sit.json) files
|
|
575
|
+
const wrappedFreeze = async function () {
|
|
576
|
+
const snapshot = await freeze()
|
|
577
|
+
|
|
578
|
+
// Write spine instance files if capsuleModuleProjectionRoot is available
|
|
579
|
+
if (capsuleModuleProjectionRoot) {
|
|
580
|
+
try {
|
|
581
|
+
// Deduplicate capsules: the capsules dict has entries keyed by both
|
|
582
|
+
// capsuleSourceLineRef and capsuleName — only process capsuleSourceLineRef keys
|
|
583
|
+
const uniqueCapsules: Record<string, any> = {}
|
|
584
|
+
for (const [key, capsule] of Object.entries(capsules)) {
|
|
585
|
+
if (key.includes(':') && /:\d+$/.test(key)) {
|
|
586
|
+
uniqueCapsules[key] = capsule
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Find root capsules — capsules that are NOT referenced as mapped dependencies
|
|
591
|
+
const mappedCapsuleNames = new Set<string>()
|
|
592
|
+
for (const [, capsule] of Object.entries(uniqueCapsules)) {
|
|
593
|
+
const cst = capsule.cst
|
|
594
|
+
if (cst?.spineContracts) {
|
|
595
|
+
for (const [, spineContract] of Object.entries(cst.spineContracts) as any) {
|
|
596
|
+
if (spineContract.propertyContracts) {
|
|
597
|
+
for (const [, propContract] of Object.entries(spineContract.propertyContracts) as any) {
|
|
598
|
+
if (propContract.properties) {
|
|
599
|
+
for (const [, propDef] of Object.entries(propContract.properties) as any) {
|
|
600
|
+
if (propDef.type === 'CapsulePropertyTypes.Mapping' && propDef.mappedModuleUri) {
|
|
601
|
+
mappedCapsuleNames.add(propDef.mappedModuleUri)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (cst?.source?.extendsCapsuleUri) {
|
|
610
|
+
mappedCapsuleNames.add(cst.source.extendsCapsuleUri)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
for (const [, capsule] of Object.entries(uniqueCapsules)) {
|
|
615
|
+
const cst = capsule.cst
|
|
616
|
+
const rootCapsuleName = cst?.source?.capsuleName
|
|
617
|
+
if (!rootCapsuleName) continue
|
|
618
|
+
|
|
619
|
+
const moduleUri = cst?.source?.moduleUri
|
|
620
|
+
if (mappedCapsuleNames.has(rootCapsuleName) || (moduleUri && mappedCapsuleNames.has(moduleUri))) {
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// This is a root capsule — write its .sit.json
|
|
625
|
+
const dirName = rootCapsuleName.replace(/\//g, '~')
|
|
626
|
+
const sitDir = join(capsuleModuleProjectionRoot, '.~o/encapsulate.dev/spine-instances', dirName)
|
|
627
|
+
const sitFilePath = join(sitDir, `root-capsule.sit.json`)
|
|
628
|
+
|
|
629
|
+
// Build the capsules map
|
|
630
|
+
const capsuleEntries: Record<string, { capsuleSourceUriLineRef: string }> = {}
|
|
631
|
+
for (const [, cap] of Object.entries(uniqueCapsules)) {
|
|
632
|
+
const capCst = cap.cst
|
|
633
|
+
if (capCst?.source?.capsuleName) {
|
|
634
|
+
capsuleEntries[capCst.source.capsuleName] = {
|
|
635
|
+
capsuleSourceUriLineRef: capCst.capsuleSourceUriLineRef
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Collect capsuleInstances from the cached root instance using an
|
|
641
|
+
// iterative stack — each instance stores its ID and parent ID from init
|
|
642
|
+
const rootInstance = await capsule.makeInstance()
|
|
643
|
+
const capsuleInstances: Record<string, { capsuleName: string, capsuleSourceUriLineRef: string, parentCapsuleSourceUriLineRefInstanceId: string }> = {}
|
|
644
|
+
|
|
645
|
+
// If the root instance has a sit with pre-populated capsuleInstances, use it directly
|
|
646
|
+
if (rootInstance.sit?.capsuleInstances && Object.keys(rootInstance.sit.capsuleInstances).length > 0) {
|
|
647
|
+
Object.assign(capsuleInstances, rootInstance.sit.capsuleInstances)
|
|
648
|
+
} else {
|
|
649
|
+
// Iterative stack-based collection from instance tree
|
|
650
|
+
const stack: Array<{ instance: any, parentId: string }> = [{ instance: rootInstance, parentId: '' }]
|
|
651
|
+
const visited = new Set<string>()
|
|
652
|
+
while (stack.length > 0) {
|
|
653
|
+
const { instance, parentId } = stack.pop()!
|
|
654
|
+
if (!instance?.capsuleSourceUriLineRefInstanceId) continue
|
|
655
|
+
const id = instance.capsuleSourceUriLineRefInstanceId
|
|
656
|
+
if (visited.has(id)) continue
|
|
657
|
+
visited.add(id)
|
|
658
|
+
|
|
659
|
+
capsuleInstances[id] = {
|
|
660
|
+
capsuleName: instance.capsuleName || '',
|
|
661
|
+
capsuleSourceUriLineRef: instance.capsuleSourceUriLineRef || '',
|
|
662
|
+
parentCapsuleSourceUriLineRefInstanceId: parentId
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (instance.extendedCapsuleInstance) {
|
|
666
|
+
stack.push({ instance: instance.extendedCapsuleInstance, parentId: id })
|
|
667
|
+
}
|
|
668
|
+
if (instance.mappedCapsuleInstances) {
|
|
669
|
+
for (const mapped of instance.mappedCapsuleInstances) {
|
|
670
|
+
stack.push({ instance: mapped, parentId: id })
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const rootInstanceId = rootInstance.capsuleSourceUriLineRefInstanceId || ''
|
|
677
|
+
|
|
678
|
+
const sitData = {
|
|
679
|
+
rootCapsule: {
|
|
680
|
+
capsuleSourceUriLineRef: cst.capsuleSourceUriLineRef,
|
|
681
|
+
capsuleSourceUriLineRefInstanceId: rootInstanceId
|
|
682
|
+
},
|
|
683
|
+
capsules: capsuleEntries,
|
|
684
|
+
capsuleInstances
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
await mkdir(sitDir, { recursive: true })
|
|
688
|
+
await writeFile(sitFilePath, JSON.stringify(sitData, null, 2), 'utf-8')
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
// Spine instance file writing is best-effort
|
|
692
|
+
console.warn('Warning: Failed to write spine instance files:', error)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return snapshot
|
|
697
|
+
}
|
|
698
|
+
|
|
573
699
|
return {
|
|
574
700
|
commonSpineContractOpts,
|
|
575
701
|
CapsulePropertyTypes,
|
|
576
702
|
makeImportStack,
|
|
577
703
|
encapsulate,
|
|
578
704
|
run,
|
|
579
|
-
freeze,
|
|
705
|
+
freeze: wrappedFreeze,
|
|
580
706
|
loadCapsule,
|
|
581
707
|
spineContractInstances, // Expose for testing
|
|
582
708
|
hoistSnapshot: async ({ snapshot }: { snapshot: any }) => {
|
|
@@ -202,6 +202,11 @@ const MODULE_GLOBAL_BUILTINS = new Set([
|
|
|
202
202
|
'Intl',
|
|
203
203
|
'WebAssembly',
|
|
204
204
|
|
|
205
|
+
// Global scope references
|
|
206
|
+
'globalThis',
|
|
207
|
+
'window',
|
|
208
|
+
'global',
|
|
209
|
+
|
|
205
210
|
// Global functions
|
|
206
211
|
'isNaN',
|
|
207
212
|
'isFinite',
|
|
@@ -241,8 +246,11 @@ export function StaticAnalyzer({
|
|
|
241
246
|
|
|
242
247
|
// Determine the cache file path based on whether the module is external or internal
|
|
243
248
|
let cacheFilePath: string
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
const isExternal = encapsulateOptions.moduleFilepath.startsWith('../')
|
|
250
|
+
const hasNodeModules = encapsulateOptions.moduleFilepath.includes('node_modules/')
|
|
251
|
+
|
|
252
|
+
if (isExternal || hasNodeModules) {
|
|
253
|
+
// External module or node_modules path - construct npm URI
|
|
246
254
|
const npmUri = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
247
255
|
if (npmUri) {
|
|
248
256
|
// Prefix with o/npmjs.com/node_modules/ for external modules
|
|
@@ -363,7 +371,7 @@ export function StaticAnalyzer({
|
|
|
363
371
|
|
|
364
372
|
const capsuleSourceLineRef = `${encapsulateOptions.moduleFilepath}:${encapsulateOptions.importStackLine}`
|
|
365
373
|
const capsuleSourceNameRef = encapsulateOptions.capsuleName && `${encapsulateOptions.moduleFilepath}:${encapsulateOptions.capsuleName}`
|
|
366
|
-
const capsuleSourceNameRefHash = capsuleSourceNameRef && createHash('
|
|
374
|
+
const capsuleSourceNameRefHash = capsuleSourceNameRef && createHash('sha256').update(capsuleSourceNameRef).digest('hex')
|
|
367
375
|
|
|
368
376
|
// Construct npm URI for the module - try for all modules
|
|
369
377
|
let moduleUri: string | null = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
@@ -525,20 +533,39 @@ export function StaticAnalyzer({
|
|
|
525
533
|
if (propName.startsWith('#')) {
|
|
526
534
|
const propertyContractUri = propName.substring(1) // Remove the '#' prefix
|
|
527
535
|
|
|
536
|
+
// Resolve relative property contract URIs to full npm URIs
|
|
537
|
+
let resolvedPropertyContractUri = propertyContractUri
|
|
538
|
+
let resolvedPropName = propName
|
|
539
|
+
if (propertyContractUri.startsWith('./') || propertyContractUri.startsWith('../')) {
|
|
540
|
+
const resolvedPath = resolve(dirname(moduleFilepath), propertyContractUri)
|
|
541
|
+
const npmUri = await constructNpmUri(resolvedPath + '.ts', spineOptions.spineFilesystemRoot)
|
|
542
|
+
if (npmUri) {
|
|
543
|
+
resolvedPropertyContractUri = npmUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
544
|
+
resolvedPropName = '#' + resolvedPropertyContractUri
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
528
548
|
if (ts.isObjectLiteralExpression(propValue)) {
|
|
529
549
|
// Create property contract entry
|
|
530
|
-
if (!spineContractDef.propertyContracts[
|
|
531
|
-
spineContractDef.propertyContracts[
|
|
532
|
-
propertyContractUri,
|
|
550
|
+
if (!spineContractDef.propertyContracts[resolvedPropName]) {
|
|
551
|
+
spineContractDef.propertyContracts[resolvedPropName] = {
|
|
552
|
+
propertyContractUri: resolvedPropertyContractUri,
|
|
533
553
|
properties: {}
|
|
534
554
|
}
|
|
535
555
|
}
|
|
536
556
|
|
|
537
|
-
// Check for 'as'
|
|
557
|
+
// Check for 'as' and 'options' properties at the property contract level
|
|
538
558
|
for (const contractProp of propValue.properties) {
|
|
539
|
-
if (ts.isPropertyAssignment(contractProp) && ts.isIdentifier(contractProp.name)
|
|
540
|
-
if (
|
|
541
|
-
|
|
559
|
+
if (ts.isPropertyAssignment(contractProp) && ts.isIdentifier(contractProp.name)) {
|
|
560
|
+
if (contractProp.name.text === 'as') {
|
|
561
|
+
if (ts.isStringLiteral(contractProp.initializer)) {
|
|
562
|
+
spineContractDef.propertyContracts[resolvedPropName].as = contractProp.initializer.text
|
|
563
|
+
}
|
|
564
|
+
} else if (contractProp.name.text === 'options') {
|
|
565
|
+
// Store literal options object on the property contract for graph queries
|
|
566
|
+
if (ts.isObjectLiteralExpression(contractProp.initializer)) {
|
|
567
|
+
spineContractDef.propertyContracts[resolvedPropName].options = await extractLiteralObject(contractProp.initializer, sourceFile, moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
568
|
+
}
|
|
542
569
|
}
|
|
543
570
|
}
|
|
544
571
|
}
|
|
@@ -629,6 +656,9 @@ export function StaticAnalyzer({
|
|
|
629
656
|
if (selfRefs.size > 0) {
|
|
630
657
|
propDef.depends = Array.from(selfRefs)
|
|
631
658
|
}
|
|
659
|
+
} else if (ts.isObjectLiteralExpression(fieldValue)) {
|
|
660
|
+
// Store literal options object in the CST for graph queries
|
|
661
|
+
propDef.options = await extractLiteralObject(fieldValue, sourceFile, moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
632
662
|
}
|
|
633
663
|
} else if (fieldName === 'kind') {
|
|
634
664
|
propDef.kind = fieldValue.getText(sourceFile)
|
|
@@ -640,7 +670,7 @@ export function StaticAnalyzer({
|
|
|
640
670
|
}
|
|
641
671
|
}
|
|
642
672
|
|
|
643
|
-
spineContractDef.propertyContracts[
|
|
673
|
+
spineContractDef.propertyContracts[resolvedPropName].properties[contractPropName] = propDef
|
|
644
674
|
}
|
|
645
675
|
}
|
|
646
676
|
}
|
|
@@ -2185,6 +2215,75 @@ function extractAndValidateAmbientReferences(
|
|
|
2185
2215
|
return ambientRefs
|
|
2186
2216
|
}
|
|
2187
2217
|
|
|
2218
|
+
// Extract a literal object from a TypeScript AST ObjectLiteralExpression.
|
|
2219
|
+
// Recursively walks the object tree and extracts string, number, boolean,
|
|
2220
|
+
// null values, arrays, and nested objects. String values that look like
|
|
2221
|
+
// relative paths (starting with ./ or ../) are resolved to npm URIs.
|
|
2222
|
+
async function extractLiteralObject(
|
|
2223
|
+
node: ts.ObjectLiteralExpression,
|
|
2224
|
+
sourceFile: ts.SourceFile,
|
|
2225
|
+
moduleFilepath: string,
|
|
2226
|
+
spineFilesystemRoot: string
|
|
2227
|
+
): Promise<Record<string, any>> {
|
|
2228
|
+
const result: Record<string, any> = {}
|
|
2229
|
+
|
|
2230
|
+
for (const prop of node.properties) {
|
|
2231
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
2232
|
+
let key: string | null = null
|
|
2233
|
+
if (ts.isIdentifier(prop.name)) key = prop.name.text
|
|
2234
|
+
else if (ts.isStringLiteral(prop.name)) key = prop.name.text
|
|
2235
|
+
if (!key) continue
|
|
2236
|
+
|
|
2237
|
+
result[key] = await extractLiteralValue(prop.initializer, sourceFile, moduleFilepath, spineFilesystemRoot)
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
return result
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// Extract a single literal value from a TS AST node.
|
|
2244
|
+
// Handles strings (with relative path resolution), numbers, booleans,
|
|
2245
|
+
// null, undefined, arrays, and nested objects.
|
|
2246
|
+
async function extractLiteralValue(
|
|
2247
|
+
node: ts.Expression,
|
|
2248
|
+
sourceFile: ts.SourceFile,
|
|
2249
|
+
moduleFilepath: string,
|
|
2250
|
+
spineFilesystemRoot: string
|
|
2251
|
+
): Promise<any> {
|
|
2252
|
+
// String literals — resolve relative paths to npm URIs
|
|
2253
|
+
if (ts.isStringLiteral(node)) {
|
|
2254
|
+
const text = node.text
|
|
2255
|
+
if (text.startsWith('./') || text.startsWith('../')) {
|
|
2256
|
+
const resolvedPath = resolve(dirname(moduleFilepath), text)
|
|
2257
|
+
const npmUri = await constructNpmUri(resolvedPath + '.ts', spineFilesystemRoot)
|
|
2258
|
+
if (npmUri) return npmUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
2259
|
+
}
|
|
2260
|
+
return text
|
|
2261
|
+
}
|
|
2262
|
+
// Numeric literals
|
|
2263
|
+
if (ts.isNumericLiteral(node)) return Number(node.text)
|
|
2264
|
+
// Boolean literals
|
|
2265
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
2266
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
2267
|
+
// Null
|
|
2268
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null
|
|
2269
|
+
// Undefined
|
|
2270
|
+
if (node.kind === ts.SyntaxKind.UndefinedKeyword) return undefined
|
|
2271
|
+
// Nested objects
|
|
2272
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
2273
|
+
return await extractLiteralObject(node, sourceFile, moduleFilepath, spineFilesystemRoot)
|
|
2274
|
+
}
|
|
2275
|
+
// Arrays
|
|
2276
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
2277
|
+
const arr: any[] = []
|
|
2278
|
+
for (const elem of node.elements) {
|
|
2279
|
+
arr.push(await extractLiteralValue(elem, sourceFile, moduleFilepath, spineFilesystemRoot))
|
|
2280
|
+
}
|
|
2281
|
+
return arr
|
|
2282
|
+
}
|
|
2283
|
+
// Fallback: store the raw expression text
|
|
2284
|
+
return node.getText(sourceFile)
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2188
2287
|
// Check if a value is a literal type
|
|
2189
2288
|
function isLiteralType(value: any): boolean {
|
|
2190
2289
|
const type = typeof value
|