@stream44.studio/encapsulate 0.4.0-rc.28 → 0.4.0-rc.30
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/package.json +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +12 -12
- package/src/encapsulate.ts +24 -7
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +94 -25
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +3 -3
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +10 -4
- package/src/static-analyzer.v0.ts +71 -46
package/package.json
CHANGED
|
@@ -174,7 +174,7 @@ export function CapsuleModuleProjector({
|
|
|
174
174
|
for (const [key, potentialCapsule] of Object.entries(capsules)) {
|
|
175
175
|
if (potentialCapsule === capsule) continue
|
|
176
176
|
|
|
177
|
-
const mappedModulePath = potentialCapsule.cst?.source?.
|
|
177
|
+
const mappedModulePath = potentialCapsule.cst?.source?.moduleUri
|
|
178
178
|
|
|
179
179
|
if (mappedModulePath && (
|
|
180
180
|
mappedModulePath === mappingValue ||
|
|
@@ -224,7 +224,7 @@ export function CapsuleModuleProjector({
|
|
|
224
224
|
for (const [key, potentialCapsule] of Object.entries(capsules)) {
|
|
225
225
|
if (potentialCapsule === capsule) continue
|
|
226
226
|
|
|
227
|
-
const mappedModulePath = potentialCapsule.cst?.source?.
|
|
227
|
+
const mappedModulePath = potentialCapsule.cst?.source?.moduleUri
|
|
228
228
|
|
|
229
229
|
if (mappedModulePath && (
|
|
230
230
|
mappedModulePath === mappingValue ||
|
|
@@ -515,7 +515,7 @@ export function CapsuleModuleProjector({
|
|
|
515
515
|
projectingCapsules.add(capsuleId)
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
timing?.record(`Projector: Start projection for ${capsule.cst.source.moduleFilepath}`)
|
|
518
|
+
timing?.record(`Projector: Start projection for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`)
|
|
519
519
|
|
|
520
520
|
// Only project capsules that have the Capsule struct property
|
|
521
521
|
const spineContract = capsule.cst.spineContracts[spineContractUri]
|
|
@@ -571,10 +571,10 @@ export function CapsuleModuleProjector({
|
|
|
571
571
|
if (allProjectedFilesExist) {
|
|
572
572
|
// Restore snapshotValues from cache
|
|
573
573
|
Object.assign(snapshotValues, merge(snapshotValues, cachedData.snapshotData))
|
|
574
|
-
timing?.record(`Projector: Cache HIT for ${capsule.cst.source.moduleFilepath}`)
|
|
574
|
+
timing?.record(`Projector: Cache HIT for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`)
|
|
575
575
|
return true
|
|
576
576
|
} else {
|
|
577
|
-
timing?.record(timing?.chalk?.yellow?.(`Projector: Cache INVALID (projected files missing) for ${capsule.cst.source.moduleFilepath}`))
|
|
577
|
+
timing?.record(timing?.chalk?.yellow?.(`Projector: Cache INVALID (projected files missing) for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`))
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
580
|
}
|
|
@@ -588,7 +588,7 @@ export function CapsuleModuleProjector({
|
|
|
588
588
|
|
|
589
589
|
// Check if this capsule has the Capsule struct (meaning it should be projected)
|
|
590
590
|
if (potentialMappedCapsule.cst?.spineContracts?.[spineContractUri]?.propertyContracts?.['#@stream44.studio/encapsulate/structs/Capsule']) {
|
|
591
|
-
// Check if this capsule's
|
|
591
|
+
// Check if this capsule's moduleUri is referenced in any mapping property
|
|
592
592
|
const mappedModulePath = potentialMappedCapsule.cst.source.moduleFilepath
|
|
593
593
|
|
|
594
594
|
for (const [propContractKey, propContract] of Object.entries(spineContract.propertyContracts)) {
|
|
@@ -612,7 +612,7 @@ export function CapsuleModuleProjector({
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
|
-
timing?.record(timing?.chalk?.red?.(`Projector: Cache MISS for ${capsule.cst.source.moduleFilepath}`))
|
|
615
|
+
timing?.record(timing?.chalk?.red?.(`Projector: Cache MISS for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`))
|
|
616
616
|
} catch (error) {
|
|
617
617
|
// Cache miss or error, proceed with projection
|
|
618
618
|
}
|
|
@@ -633,13 +633,13 @@ export function CapsuleModuleProjector({
|
|
|
633
633
|
|
|
634
634
|
const cstJson = JSON.stringify(targetCapsule.cst, null, 4)
|
|
635
635
|
const crtJson = JSON.stringify(targetCapsule.crt || {}, null, 4)
|
|
636
|
-
const
|
|
636
|
+
const moduleUri = targetCapsule.cst.source.moduleUri
|
|
637
637
|
const importStackLine = targetCapsule.cst.source.importStackLine
|
|
638
638
|
|
|
639
|
-
// Replace importMeta: import.meta with moduleFilepath: '...'
|
|
639
|
+
// Replace importMeta: import.meta with moduleFilepath: '...' (using npm URI)
|
|
640
640
|
expression = expression.replace(
|
|
641
641
|
/importMeta:\s*import\.meta/g,
|
|
642
|
-
`moduleFilepath: '${
|
|
642
|
+
`moduleFilepath: '${moduleUri}'`
|
|
643
643
|
)
|
|
644
644
|
|
|
645
645
|
// Replace importStack: makeImportStack() with importStackLine: ..., crt: {...}, cst: {...}
|
|
@@ -1662,7 +1662,7 @@ ${mappedDefaultExport}
|
|
|
1662
1662
|
await projectionCacheStore.writeFile(cacheFilename, JSON.stringify(cacheData, null, 2))
|
|
1663
1663
|
} catch (error) {
|
|
1664
1664
|
// Cache write error, continue without failing
|
|
1665
|
-
console.warn(`Warning: Failed to write projection cache for ${capsule.cst.source.moduleFilepath}:`, error)
|
|
1665
|
+
console.warn(`Warning: Failed to write projection cache for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}:`, error)
|
|
1666
1666
|
}
|
|
1667
1667
|
}
|
|
1668
1668
|
|
|
@@ -1749,7 +1749,7 @@ capsule['#'] = ${JSON.stringify(capsuleName)}
|
|
|
1749
1749
|
await projectionStore.writeFile(projectedPath, capsuleFileContent)
|
|
1750
1750
|
}
|
|
1751
1751
|
} catch (error) {
|
|
1752
|
-
console.warn(`Warning: Failed to write projection cache for capsule ${registryCapsule.cst.source.moduleFilepath}:`, error)
|
|
1752
|
+
console.warn(`Warning: Failed to write projection cache for capsule ${registryCapsule.cst.source.moduleUri || registryCapsule.cst.source.moduleFilepath}:`, error)
|
|
1753
1753
|
}
|
|
1754
1754
|
}
|
|
1755
1755
|
}
|
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 = 22
|
|
5
5
|
|
|
6
6
|
type TSpineOptions = {
|
|
7
7
|
spineFilesystemRoot?: string,
|
|
@@ -505,21 +505,30 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
505
505
|
|
|
506
506
|
if (typeof importStackLine !== 'number') throw new Error(`Could not determine importStackLine from options`)
|
|
507
507
|
|
|
508
|
-
|
|
508
|
+
// Temporary filesystem-based ref used only for passing to static analyzer
|
|
509
|
+
const fsBasedRef = `${moduleFilepath}:${importStackLine}`
|
|
509
510
|
|
|
510
511
|
spine.spineOptions.timing?.record(`Encapsulate: Start for ${moduleFilepath}`)
|
|
511
512
|
|
|
512
|
-
const
|
|
513
|
+
const parseResult = await spine.spineOptions.staticAnalyzer?.parseModule({
|
|
513
514
|
spineOptions: spine.spineOptions,
|
|
514
515
|
encapsulateOptions: {
|
|
515
516
|
moduleFilepath,
|
|
516
517
|
importStackLine,
|
|
517
|
-
capsuleSourceLineRef,
|
|
518
|
+
capsuleSourceLineRef: fsBasedRef,
|
|
518
519
|
capsuleName: options.capsuleName,
|
|
519
520
|
ambientReferences: options.ambientReferences,
|
|
520
521
|
cacheBustVersion: CACHE_BUST_VERSION
|
|
521
522
|
}
|
|
522
|
-
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
// Use moduleUri from static analyzer for npm URI-based capsuleSourceLineRef
|
|
526
|
+
const moduleUri = parseResult?.moduleUri
|
|
527
|
+
const capsuleSourceLineRef = moduleUri
|
|
528
|
+
? `${moduleUri}:${importStackLine}`
|
|
529
|
+
: fsBasedRef
|
|
530
|
+
|
|
531
|
+
const { csts, crts } = parseResult || {
|
|
523
532
|
csts: options.cst ? { [capsuleSourceLineRef]: options.cst } : undefined,
|
|
524
533
|
crts: options.crt ? { [capsuleSourceLineRef]: options.crt } : undefined
|
|
525
534
|
}
|
|
@@ -722,12 +731,20 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
722
731
|
const ownSelf = merge({}, defaultInstance, defaultPropertyValues, ...Object.values(mergedValuesByContract))
|
|
723
732
|
|
|
724
733
|
// Convert relative paths to absolute for metadata exposure
|
|
734
|
+
// When a CST exists with a source moduleFilepath, use it to derive the absolute path.
|
|
735
|
+
// This ensures that projected capsules (loaded from .~o/encapsulate.dev/caps/...)
|
|
736
|
+
// still expose the original source filepath, not the projected filepath.
|
|
737
|
+
const originalAbsoluteModuleFilepath = cst?.source?.moduleFilepath
|
|
738
|
+
? (cst.source.moduleFilepath.startsWith('/')
|
|
739
|
+
? cst.source.moduleFilepath
|
|
740
|
+
: join(spine.spineOptions.spineFilesystemRoot || '', cst.source.moduleFilepath))
|
|
741
|
+
: absoluteModuleFilepath
|
|
725
742
|
const absoluteCapsuleSourceLineRef = `${absoluteModuleFilepath}:${importStackLine}`
|
|
726
743
|
const capsuleMetadataStruct: Record<string, any> = {
|
|
727
744
|
capsuleName: encapsulateOptions.capsuleName,
|
|
728
745
|
capsuleSourceLineRef: absoluteCapsuleSourceLineRef,
|
|
729
746
|
capsuleSourceNameRefHash: cst?.capsuleSourceNameRefHash,
|
|
730
|
-
moduleFilepath:
|
|
747
|
+
moduleFilepath: originalAbsoluteModuleFilepath,
|
|
731
748
|
// Root capsule metadata will be populated after extends chain is resolved
|
|
732
749
|
rootCapsule: {
|
|
733
750
|
capsuleName: undefined as string | undefined,
|
|
@@ -845,7 +862,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
845
862
|
const resolvedRootCapsule = rootCapsule || {
|
|
846
863
|
capsuleName: encapsulateOptions.capsuleName!,
|
|
847
864
|
capsuleSourceLineRef: absoluteCapsuleSourceLineRef,
|
|
848
|
-
moduleFilepath:
|
|
865
|
+
moduleFilepath: originalAbsoluteModuleFilepath
|
|
849
866
|
}
|
|
850
867
|
capsuleMetadataStruct.rootCapsule.capsuleName = resolvedRootCapsule.capsuleName
|
|
851
868
|
capsuleMetadataStruct.rootCapsule.capsuleSourceLineRef = resolvedRootCapsule.capsuleSourceLineRef
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CapsulePropertyTypes } from "../../encapsulate"
|
|
2
2
|
import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static.v0"
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs"
|
|
4
|
+
import { dirname, relative, join } from "node:path"
|
|
3
5
|
|
|
4
6
|
type CallerContext = {
|
|
5
7
|
capsuleSourceLineRef: string
|
|
@@ -8,9 +10,9 @@ type CallerContext = {
|
|
|
8
10
|
capsuleSourceNameRefHash?: string
|
|
9
11
|
capsuleSourceUriLineRefInstanceId?: string
|
|
10
12
|
prop?: string
|
|
11
|
-
|
|
13
|
+
fileUri?: string
|
|
12
14
|
line?: number
|
|
13
|
-
stack?: Array<{ function?: string,
|
|
15
|
+
stack?: Array<{ function?: string, fileUri?: string, line?: number, column?: number }>
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function CapsuleMembrane(target: Record<string, any>, hooks?: {
|
|
@@ -140,11 +142,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
140
142
|
|
|
141
143
|
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
142
144
|
// options can be a function or an object for regular mappings
|
|
143
|
-
// Always pass { self, constants } - self
|
|
145
|
+
// Always pass { self, constants } - self contains full parent self when depends is specified,
|
|
146
|
+
// otherwise just the Capsule metadata struct (moduleFilepath, capsuleName, etc.)
|
|
144
147
|
const optionsFn = property.definition.options
|
|
148
|
+
const capsuleStructKey = '#@stream44.studio/encapsulate/structs/Capsule'
|
|
149
|
+
const minimalSelf = this.self[capsuleStructKey]
|
|
150
|
+
? { [capsuleStructKey]: this.self[capsuleStructKey] }
|
|
151
|
+
: {}
|
|
145
152
|
const mappingOptions = property.definition.delegateOptions
|
|
146
153
|
|| (typeof optionsFn === 'function'
|
|
147
|
-
? await optionsFn({ self: property.definition.depends ? this.self :
|
|
154
|
+
? await optionsFn({ self: property.definition.depends ? this.self : minimalSelf, constants })
|
|
148
155
|
: optionsFn)
|
|
149
156
|
|
|
150
157
|
// Check for existing instance in registry - reuse if available (regardless of options)
|
|
@@ -180,7 +187,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
180
187
|
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
181
188
|
if (stackFrames.length > 0) {
|
|
182
189
|
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
183
|
-
callerCtx.
|
|
190
|
+
callerCtx.fileUri = callerInfo.fileUri
|
|
184
191
|
callerCtx.line = callerInfo.line
|
|
185
192
|
callerCtx.stack = stackFrames
|
|
186
193
|
}
|
|
@@ -307,7 +314,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
307
314
|
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
308
315
|
if (stackFrames.length > 0) {
|
|
309
316
|
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
310
|
-
callerCtx.
|
|
317
|
+
callerCtx.fileUri = callerInfo.fileUri
|
|
311
318
|
callerCtx.line = callerInfo.line
|
|
312
319
|
callerCtx.stack = stackFrames
|
|
313
320
|
}
|
|
@@ -370,7 +377,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
370
377
|
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
371
378
|
if (stackFrames.length > 0) {
|
|
372
379
|
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
373
|
-
callerCtx.
|
|
380
|
+
callerCtx.fileUri = callerInfo.fileUri
|
|
374
381
|
callerCtx.line = callerInfo.line
|
|
375
382
|
callerCtx.stack = stackFrames
|
|
376
383
|
}
|
|
@@ -873,8 +880,8 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
873
880
|
if (callerCtx.prop) {
|
|
874
881
|
event.caller.prop = callerCtx.prop
|
|
875
882
|
}
|
|
876
|
-
if (callerCtx.
|
|
877
|
-
event.caller.
|
|
883
|
+
if (callerCtx.fileUri) {
|
|
884
|
+
event.caller.fileUri = callerCtx.fileUri
|
|
878
885
|
}
|
|
879
886
|
if (callerCtx.line) {
|
|
880
887
|
event.caller.line = callerCtx.line
|
|
@@ -970,9 +977,62 @@ CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/Capsul
|
|
|
970
977
|
|
|
971
978
|
|
|
972
979
|
|
|
973
|
-
|
|
980
|
+
// Cache for synchronous npm URI lookups (directory -> package name or null)
|
|
981
|
+
const npmUriCache = new Map<string, string | null>()
|
|
982
|
+
|
|
983
|
+
function constructNpmUriSync(absoluteFilepath: string): string | null {
|
|
984
|
+
// Check for /node_modules/ in the path — use the last occurrence to handle nested node_modules
|
|
985
|
+
const nodeModulesMarker = '/node_modules/'
|
|
986
|
+
const lastIdx = absoluteFilepath.lastIndexOf(nodeModulesMarker)
|
|
987
|
+
if (lastIdx !== -1) {
|
|
988
|
+
return absoluteFilepath.substring(lastIdx + nodeModulesMarker.length)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
let currentDir = dirname(absoluteFilepath)
|
|
992
|
+
const maxDepth = 20
|
|
993
|
+
|
|
994
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
995
|
+
if (npmUriCache.has(currentDir)) {
|
|
996
|
+
const cachedName = npmUriCache.get(currentDir)
|
|
997
|
+
if (cachedName) {
|
|
998
|
+
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
999
|
+
return `${cachedName}/${relativeFromPackage}`
|
|
1000
|
+
}
|
|
1001
|
+
// null means no package.json with name found at this level, continue up
|
|
1002
|
+
const parentDir = dirname(currentDir)
|
|
1003
|
+
if (parentDir === currentDir) break
|
|
1004
|
+
currentDir = parentDir
|
|
1005
|
+
continue
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const packageJsonPath = join(currentDir, 'package.json')
|
|
1009
|
+
try {
|
|
1010
|
+
if (existsSync(packageJsonPath)) {
|
|
1011
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
1012
|
+
const packageName = packageJson.name
|
|
1013
|
+
npmUriCache.set(currentDir, packageName || null)
|
|
1014
|
+
if (packageName) {
|
|
1015
|
+
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
1016
|
+
return `${packageName}/${relativeFromPackage}`
|
|
1017
|
+
}
|
|
1018
|
+
} else {
|
|
1019
|
+
npmUriCache.set(currentDir, null)
|
|
1020
|
+
}
|
|
1021
|
+
} catch {
|
|
1022
|
+
npmUriCache.set(currentDir, null)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const parentDir = dirname(currentDir)
|
|
1026
|
+
if (parentDir === currentDir) break
|
|
1027
|
+
currentDir = parentDir
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return null
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, fileUri?: string, line?: number, column?: number }> {
|
|
974
1034
|
const lines = stack.split('\n')
|
|
975
|
-
const result: Array<{ function?: string,
|
|
1035
|
+
const result: Array<{ function?: string, fileUri?: string, line?: number, column?: number }> = []
|
|
976
1036
|
|
|
977
1037
|
// Skip first line (Error message), then collect ALL frames
|
|
978
1038
|
for (let i = 1; i < lines.length; i++) {
|
|
@@ -984,9 +1044,11 @@ function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Arra
|
|
|
984
1044
|
// "at functionName (file:line:column)"
|
|
985
1045
|
const match = line.match(/at\s+(.+)/)
|
|
986
1046
|
if (match) {
|
|
987
|
-
const frame: { function?: string,
|
|
1047
|
+
const frame: { function?: string, fileUri?: string, line?: number, column?: number } = {}
|
|
988
1048
|
const content = match[1]
|
|
989
1049
|
|
|
1050
|
+
let rawFilepath: string | undefined
|
|
1051
|
+
|
|
990
1052
|
// Try to extract function name and location
|
|
991
1053
|
const funcMatch = content.match(/^(.+?)\s+\((.+)\)$/)
|
|
992
1054
|
if (funcMatch) {
|
|
@@ -999,7 +1061,7 @@ function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Arra
|
|
|
999
1061
|
const location = funcMatch[2]
|
|
1000
1062
|
const locMatch = location.match(/^(.+):(\d+):(\d+)$/)
|
|
1001
1063
|
if (locMatch) {
|
|
1002
|
-
|
|
1064
|
+
rawFilepath = locMatch[1]
|
|
1003
1065
|
frame.line = parseInt(locMatch[2], 10)
|
|
1004
1066
|
frame.column = parseInt(locMatch[3], 10)
|
|
1005
1067
|
}
|
|
@@ -1007,25 +1069,32 @@ function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Arra
|
|
|
1007
1069
|
// No function name: "/path/to/file:line:column"
|
|
1008
1070
|
const locMatch = content.match(/^(.+):(\d+):(\d+)$/)
|
|
1009
1071
|
if (locMatch) {
|
|
1010
|
-
|
|
1072
|
+
rawFilepath = locMatch[1]
|
|
1011
1073
|
frame.line = parseInt(locMatch[2], 10)
|
|
1012
1074
|
frame.column = parseInt(locMatch[3], 10)
|
|
1013
1075
|
}
|
|
1014
1076
|
}
|
|
1015
1077
|
|
|
1016
|
-
// Convert absolute
|
|
1017
|
-
if (
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
//
|
|
1021
|
-
|
|
1022
|
-
|
|
1078
|
+
// Convert absolute filepaths to npm URIs
|
|
1079
|
+
if (rawFilepath) {
|
|
1080
|
+
const npmUri = constructNpmUriSync(rawFilepath)
|
|
1081
|
+
if (npmUri) {
|
|
1082
|
+
// Strip file extension from URI for consistency
|
|
1083
|
+
frame.fileUri = npmUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
1084
|
+
} else if (spineFilesystemRoot && rawFilepath.startsWith(spineFilesystemRoot)) {
|
|
1085
|
+
// Fallback: use relative path from spine root if npm URI not resolvable
|
|
1086
|
+
let relativePath = rawFilepath.slice(spineFilesystemRoot.length)
|
|
1087
|
+
if (relativePath.startsWith('/')) {
|
|
1088
|
+
relativePath = relativePath.slice(1)
|
|
1023
1089
|
}
|
|
1090
|
+
frame.fileUri = relativePath
|
|
1091
|
+
} else {
|
|
1092
|
+
frame.fileUri = rawFilepath
|
|
1024
1093
|
}
|
|
1025
1094
|
}
|
|
1026
1095
|
|
|
1027
1096
|
// Include all frames, even if incomplete
|
|
1028
|
-
if (frame.
|
|
1097
|
+
if (frame.fileUri || frame.function) {
|
|
1029
1098
|
result.push(frame)
|
|
1030
1099
|
}
|
|
1031
1100
|
}
|
|
@@ -1033,14 +1102,14 @@ function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Arra
|
|
|
1033
1102
|
return result
|
|
1034
1103
|
}
|
|
1035
1104
|
|
|
1036
|
-
function extractCallerInfo(stack: Array<{ function?: string,
|
|
1105
|
+
function extractCallerInfo(stack: Array<{ function?: string, fileUri?: string, line?: number, column?: number }>, offset: number = 0) {
|
|
1037
1106
|
// Use offset to skip frames in the stack
|
|
1038
1107
|
// offset 0 = first frame, offset 1 = second frame, etc.
|
|
1039
1108
|
|
|
1040
1109
|
if (offset < stack.length) {
|
|
1041
1110
|
const frame = stack[offset]
|
|
1042
1111
|
return {
|
|
1043
|
-
|
|
1112
|
+
fileUri: frame.fileUri,
|
|
1044
1113
|
line: frame.line
|
|
1045
1114
|
}
|
|
1046
1115
|
}
|
|
@@ -1048,7 +1117,7 @@ function extractCallerInfo(stack: Array<{ function?: string, filepath?: string,
|
|
|
1048
1117
|
// Fallback to first frame if offset is out of bounds
|
|
1049
1118
|
if (stack.length > 0) {
|
|
1050
1119
|
return {
|
|
1051
|
-
|
|
1120
|
+
fileUri: stack[0].fileUri,
|
|
1052
1121
|
line: stack[0].line
|
|
1053
1122
|
}
|
|
1054
1123
|
}
|
|
@@ -308,8 +308,8 @@ prop: {
|
|
|
308
308
|
- **`options`** — forwarded to the mapped capsule. Keys starting with `'#'` target the mapped capsule's own property contracts. Keys without `'#'` are matched against capsule names deeper in the mapping tree (nested capsule-name-targeted options).
|
|
309
309
|
- **`options({ self, constants })`** — when `options` is a function, it receives `{ self, constants }`.
|
|
310
310
|
- `constants` — all `Literal`/`String` values from the mapped capsule's definition.
|
|
311
|
-
- `self` — the
|
|
312
|
-
- **`depends`** — array of sibling property names that must be resolved before this mapping's `options` function runs. Enables `options({ self })` to access already-resolved siblings (e.g. `self.$auth.realm`)
|
|
311
|
+
- `self` — always contains the Capsule metadata struct (`self['#@stream44.studio/encapsulate/structs/Capsule']` with `moduleFilepath`, `capsuleName`, etc.). When `depends` is specified, `self` also contains the full parent capsule's resolved sibling mappings.
|
|
312
|
+
- **`depends`** — array of sibling property names that must be resolved before this mapping's `options` function runs. Enables `options({ self })` to access already-resolved siblings (e.g. `self.$auth.realm`). Can be declared explicitly or auto-injected by the static analyzer when it detects `self.<name>` references in the options function body.
|
|
313
313
|
- **Instance reuse** — named capsules are registered in an instance registry. If a capsule with the same name is mapped multiple times without options, the existing instance is reused via a deferred proxy.
|
|
314
314
|
|
|
315
315
|
Mapped capsules are accessible via `this.<prop>` (unwrapped API) and `api.<prop>` (raw instance with `.api`).
|
|
@@ -397,7 +397,7 @@ Both implement the same property mapping logic. The difference is observability.
|
|
|
397
397
|
| `call` | Function invoked | `{ target, args, eventIndex }` |
|
|
398
398
|
| `call-result` | Function returns | `{ target, result, callEventIndex }` |
|
|
399
399
|
|
|
400
|
-
Events include `caller` context (source capsule, property,
|
|
400
|
+
Events include `caller` context (source capsule, property, fileUri, line) when `enableCallerStackInference` is enabled. Memoized results are tagged with `memoized: true`.
|
|
401
401
|
|
|
402
402
|
### SpineRuntime & run()
|
|
403
403
|
|
|
@@ -129,8 +129,9 @@ export class ContractCapsuleInstanceFactory {
|
|
|
129
129
|
if (!this.spineFilesystemRoot) throw new Error(`'spineFilesystemRoot' not set!`)
|
|
130
130
|
if (!this.importCapsule) throw new Error(`'importCapsule' not set!`)
|
|
131
131
|
|
|
132
|
-
// Use
|
|
133
|
-
|
|
132
|
+
// Use cst.source.moduleFilepath (always filesystem-relative) for path resolution.
|
|
133
|
+
// encapsulateOptions.moduleFilepath may be an npm URI when loaded from projected files.
|
|
134
|
+
const moduleFilepath = this.capsule.cst?.source?.moduleFilepath || this.capsule.encapsulateOptions?.moduleFilepath
|
|
134
135
|
if (!moduleFilepath) throw new Error(`'moduleFilepath' not available on capsule!`)
|
|
135
136
|
|
|
136
137
|
const parentPath = join(this.spineFilesystemRoot, moduleFilepath)
|
|
@@ -197,11 +198,16 @@ export class ContractCapsuleInstanceFactory {
|
|
|
197
198
|
|
|
198
199
|
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
199
200
|
// options can be a function or an object for regular mappings
|
|
200
|
-
// Always pass { self, constants } - self
|
|
201
|
+
// Always pass { self, constants } - self contains full parent self when depends is specified,
|
|
202
|
+
// otherwise just the Capsule metadata struct (moduleFilepath, capsuleName, etc.)
|
|
201
203
|
const optionsFn = property.definition.options
|
|
204
|
+
const capsuleStructKey = '#@stream44.studio/encapsulate/structs/Capsule'
|
|
205
|
+
const minimalSelf = this.self[capsuleStructKey]
|
|
206
|
+
? { [capsuleStructKey]: this.self[capsuleStructKey] }
|
|
207
|
+
: {}
|
|
202
208
|
const mappingOptions = property.definition.delegateOptions
|
|
203
209
|
|| (typeof optionsFn === 'function'
|
|
204
|
-
? await optionsFn({ self: property.definition.depends ? this.self :
|
|
210
|
+
? await optionsFn({ self: property.definition.depends ? this.self : minimalSelf, constants })
|
|
205
211
|
: optionsFn)
|
|
206
212
|
|
|
207
213
|
// Check for existing instance in registry - reuse if available when no options
|
|
@@ -244,25 +244,13 @@ export function StaticAnalyzer({
|
|
|
244
244
|
|
|
245
245
|
const moduleFilepath = join(spineOptions.spineFilesystemRoot, encapsulateOptions.moduleFilepath)
|
|
246
246
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const npmUri = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
255
|
-
if (npmUri) {
|
|
256
|
-
// Prefix with o/npmjs.com/node_modules/ for external modules
|
|
257
|
-
cacheFilePath = `o/npmjs.com/node_modules/${npmUri}`
|
|
258
|
-
} else {
|
|
259
|
-
// Fallback to normalized path if npm URI construction fails
|
|
260
|
-
cacheFilePath = normalize(encapsulateOptions.moduleFilepath).replace(/^\.\.\//, '').replace(/\.\.\//g, '')
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
// Internal module - use relative path as-is
|
|
264
|
-
cacheFilePath = encapsulateOptions.moduleFilepath
|
|
265
|
-
}
|
|
247
|
+
// Construct npm URI for the module upfront — used for cache paths and CST keys
|
|
248
|
+
const rawModuleUri: string = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot) || encapsulateOptions.moduleFilepath
|
|
249
|
+
// Strip file extension from URI
|
|
250
|
+
const moduleUriWithoutExt = rawModuleUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
251
|
+
|
|
252
|
+
// Cache file path always uses npm URI (never filesystem-relative paths)
|
|
253
|
+
const cacheFilePath = moduleUriWithoutExt
|
|
266
254
|
|
|
267
255
|
const capsuleSourceLineRef = `${cacheFilePath}:${encapsulateOptions.importStackLine}`
|
|
268
256
|
|
|
@@ -297,7 +285,8 @@ export function StaticAnalyzer({
|
|
|
297
285
|
timing?.record(`StaticAnalyzer: Cache HIT for ${encapsulateOptions.moduleFilepath}`)
|
|
298
286
|
return {
|
|
299
287
|
csts: cachedCsts,
|
|
300
|
-
crts: JSON.parse(crtsContent)
|
|
288
|
+
crts: JSON.parse(crtsContent),
|
|
289
|
+
moduleUri: moduleUriWithoutExt
|
|
301
290
|
}
|
|
302
291
|
}
|
|
303
292
|
}
|
|
@@ -369,24 +358,12 @@ export function StaticAnalyzer({
|
|
|
369
358
|
continue
|
|
370
359
|
}
|
|
371
360
|
|
|
372
|
-
|
|
373
|
-
const
|
|
361
|
+
// Use npm URI for all CST references (never filesystem-relative paths)
|
|
362
|
+
const capsuleSourceLineRef = `${moduleUriWithoutExt}:${encapsulateOptions.importStackLine}`
|
|
363
|
+
const capsuleSourceNameRef = encapsulateOptions.capsuleName && `${moduleUriWithoutExt}:${encapsulateOptions.capsuleName}`
|
|
374
364
|
const capsuleSourceNameRefHash = capsuleSourceNameRef && createHash('sha256').update(capsuleSourceNameRef).digest('hex')
|
|
375
365
|
|
|
376
|
-
|
|
377
|
-
let moduleUri: string | null = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
378
|
-
|
|
379
|
-
// If npm URI construction failed, fall back to moduleFilepath
|
|
380
|
-
if (!moduleUri) {
|
|
381
|
-
moduleUri = encapsulateOptions.moduleFilepath
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Strip file extension from URI
|
|
385
|
-
const moduleUriWithoutExt = moduleUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
386
|
-
const capsuleSourceUriLineRef = `${moduleUriWithoutExt}:${encapsulateOptions.importStackLine}`
|
|
387
|
-
|
|
388
|
-
// Store moduleUri without extension
|
|
389
|
-
moduleUri = moduleUriWithoutExt
|
|
366
|
+
const capsuleSourceUriLineRef = capsuleSourceLineRef
|
|
390
367
|
|
|
391
368
|
// Extract the capsule expression text from the source
|
|
392
369
|
const capsuleExpression = call.getText(sourceFile)
|
|
@@ -407,7 +384,7 @@ export function StaticAnalyzer({
|
|
|
407
384
|
capsuleSourceUriLineRef,
|
|
408
385
|
source: {
|
|
409
386
|
moduleFilepath: encapsulateOptions.moduleFilepath,
|
|
410
|
-
moduleUri,
|
|
387
|
+
moduleUri: moduleUriWithoutExt,
|
|
411
388
|
capsuleName: encapsulateOptions.capsuleName,
|
|
412
389
|
declarationLine,
|
|
413
390
|
importStackLine: encapsulateOptions.importStackLine,
|
|
@@ -760,6 +737,7 @@ export function StaticAnalyzer({
|
|
|
760
737
|
return {
|
|
761
738
|
csts,
|
|
762
739
|
crts,
|
|
740
|
+
moduleUri: moduleUriWithoutExt
|
|
763
741
|
}
|
|
764
742
|
}
|
|
765
743
|
}
|
|
@@ -1028,7 +1006,7 @@ function extractModuleLocalCode(
|
|
|
1028
1006
|
if (funcDecl) {
|
|
1029
1007
|
// Analyze the function to see if it's self-contained
|
|
1030
1008
|
const dependencies = new Set<string>()
|
|
1031
|
-
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
|
|
1009
|
+
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies, moduleLocalVariables)
|
|
1032
1010
|
|
|
1033
1011
|
if (isContained) {
|
|
1034
1012
|
// Mark this as module-local in ambient references
|
|
@@ -1056,9 +1034,33 @@ function extractModuleLocalCode(
|
|
|
1056
1034
|
// Collect the main function
|
|
1057
1035
|
collectFunction(name)
|
|
1058
1036
|
|
|
1059
|
-
// Collect all dependencies
|
|
1037
|
+
// Collect all dependencies (functions and variables)
|
|
1060
1038
|
for (const dep of dependencies) {
|
|
1061
1039
|
collectFunction(dep)
|
|
1040
|
+
|
|
1041
|
+
// Also collect module-local variable dependencies
|
|
1042
|
+
const depVarDecl = moduleLocalVariables.get(dep)
|
|
1043
|
+
if (depVarDecl && !processed.has(dep)) {
|
|
1044
|
+
processed.add(dep)
|
|
1045
|
+
const varStatement = depVarDecl.parent?.parent
|
|
1046
|
+
if (varStatement && ts.isVariableStatement(varStatement)) {
|
|
1047
|
+
const varCode = varStatement.getText(sourceFile)
|
|
1048
|
+
collectedCode.push(varCode)
|
|
1049
|
+
if (!moduleLocalCode[dep]) {
|
|
1050
|
+
moduleLocalCode[dep] = varCode
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// Mark the variable dependency in ambient references
|
|
1054
|
+
if (!ambientReferences[dep]) {
|
|
1055
|
+
ambientReferences[dep] = { type: 'module-local' }
|
|
1056
|
+
}
|
|
1057
|
+
// Recursively collect transitive variable dependencies
|
|
1058
|
+
collectTransitiveVariableDependencies(
|
|
1059
|
+
depVarDecl, sourceFile, importMap, assignmentMap,
|
|
1060
|
+
moduleLocalFunctions, moduleLocalVariables,
|
|
1061
|
+
ambientReferences, moduleLocalCode
|
|
1062
|
+
)
|
|
1063
|
+
}
|
|
1062
1064
|
}
|
|
1063
1065
|
|
|
1064
1066
|
// Store the collected code (main function with all dependencies)
|
|
@@ -1109,7 +1111,7 @@ function extractModuleLocalCode(
|
|
|
1109
1111
|
|
|
1110
1112
|
// Analyze if it's self-contained
|
|
1111
1113
|
const dependencies = new Set<string>()
|
|
1112
|
-
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
|
|
1114
|
+
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies, moduleLocalVariables)
|
|
1113
1115
|
|
|
1114
1116
|
if (isContained) {
|
|
1115
1117
|
// Add this function to moduleLocalCode
|
|
@@ -1236,14 +1238,15 @@ function collectTransitiveVariableDependencies(
|
|
|
1236
1238
|
visit(varDecl.initializer)
|
|
1237
1239
|
}
|
|
1238
1240
|
|
|
1239
|
-
// Analyze if a function is self-contained (only depends on other module-local functions or builtins)
|
|
1241
|
+
// Analyze if a function is self-contained (only depends on other module-local functions, variables, or builtins)
|
|
1240
1242
|
function analyzeFunctionDependencies(
|
|
1241
1243
|
funcDecl: ts.FunctionDeclaration,
|
|
1242
1244
|
sourceFile: ts.SourceFile,
|
|
1243
1245
|
importMap: Map<string, { importSpecifier: string, moduleUri: string }>,
|
|
1244
1246
|
assignmentMap: Map<string, { importSpecifier: string, moduleUri: string }>,
|
|
1245
1247
|
moduleLocalFunctions: Map<string, ts.FunctionDeclaration>,
|
|
1246
|
-
dependencies: Set<string
|
|
1248
|
+
dependencies: Set<string>,
|
|
1249
|
+
moduleLocalVariables?: Map<string, ts.VariableDeclaration>
|
|
1247
1250
|
): boolean {
|
|
1248
1251
|
const localIdentifiers = new Set<string>()
|
|
1249
1252
|
const nestedFunctionScopes = new Map<ts.Node, Set<string>>()
|
|
@@ -1370,6 +1373,12 @@ function analyzeFunctionDependencies(
|
|
|
1370
1373
|
return
|
|
1371
1374
|
}
|
|
1372
1375
|
|
|
1376
|
+
// Check if it's a module-local variable - add as dependency
|
|
1377
|
+
if (moduleLocalVariables?.has(identifierName)) {
|
|
1378
|
+
dependencies.add(identifierName)
|
|
1379
|
+
return
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1373
1382
|
// Check if it's a module-global builtin - allowed
|
|
1374
1383
|
if (MODULE_GLOBAL_BUILTINS.has(identifierName)) {
|
|
1375
1384
|
return
|
|
@@ -1703,7 +1712,7 @@ function extractCapsuleAmbientReferences(
|
|
|
1703
1712
|
if (funcDecl) {
|
|
1704
1713
|
// Analyze if it's self-contained
|
|
1705
1714
|
const dependencies = new Set<string>()
|
|
1706
|
-
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
|
|
1715
|
+
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies, moduleLocalVariables)
|
|
1707
1716
|
|
|
1708
1717
|
if (isContained) {
|
|
1709
1718
|
// Mark as module-local
|
|
@@ -1711,7 +1720,7 @@ function extractCapsuleAmbientReferences(
|
|
|
1711
1720
|
type: 'module-local'
|
|
1712
1721
|
}
|
|
1713
1722
|
|
|
1714
|
-
// Add import dependencies from the function's body
|
|
1723
|
+
// Add import/variable dependencies from the function's body
|
|
1715
1724
|
for (const depName of dependencies) {
|
|
1716
1725
|
if (!ambientRefs[depName]) {
|
|
1717
1726
|
const depImportInfo = importMap.get(depName)
|
|
@@ -1729,6 +1738,14 @@ function extractCapsuleAmbientReferences(
|
|
|
1729
1738
|
importSpecifier: depAssignmentInfo.importSpecifier,
|
|
1730
1739
|
moduleUri: depAssignmentInfo.moduleUri
|
|
1731
1740
|
}
|
|
1741
|
+
} else if (moduleLocalVariables.has(depName)) {
|
|
1742
|
+
ambientRefs[depName] = {
|
|
1743
|
+
type: 'module-local'
|
|
1744
|
+
}
|
|
1745
|
+
} else if (moduleLocalFunctions.has(depName)) {
|
|
1746
|
+
ambientRefs[depName] = {
|
|
1747
|
+
type: 'module-local'
|
|
1748
|
+
}
|
|
1732
1749
|
}
|
|
1733
1750
|
}
|
|
1734
1751
|
}
|
|
@@ -2075,7 +2092,7 @@ function extractAndValidateAmbientReferences(
|
|
|
2075
2092
|
if (funcDecl) {
|
|
2076
2093
|
// Analyze if it's self-contained
|
|
2077
2094
|
const dependencies = new Set<string>()
|
|
2078
|
-
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
|
|
2095
|
+
const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies, moduleLocalVariables)
|
|
2079
2096
|
|
|
2080
2097
|
if (isContained) {
|
|
2081
2098
|
// Mark as module-local
|
|
@@ -2083,7 +2100,7 @@ function extractAndValidateAmbientReferences(
|
|
|
2083
2100
|
type: 'module-local'
|
|
2084
2101
|
}
|
|
2085
2102
|
|
|
2086
|
-
// Add import dependencies from the function's body
|
|
2103
|
+
// Add import/variable dependencies from the function's body
|
|
2087
2104
|
for (const depName of dependencies) {
|
|
2088
2105
|
if (!ambientRefs[depName]) {
|
|
2089
2106
|
const depImportInfo = importMap.get(depName)
|
|
@@ -2101,6 +2118,14 @@ function extractAndValidateAmbientReferences(
|
|
|
2101
2118
|
importSpecifier: depAssignmentInfo.importSpecifier,
|
|
2102
2119
|
moduleUri: depAssignmentInfo.moduleUri
|
|
2103
2120
|
}
|
|
2121
|
+
} else if (moduleLocalVariables.has(depName)) {
|
|
2122
|
+
ambientRefs[depName] = {
|
|
2123
|
+
type: 'module-local'
|
|
2124
|
+
}
|
|
2125
|
+
} else if (moduleLocalFunctions.has(depName)) {
|
|
2126
|
+
ambientRefs[depName] = {
|
|
2127
|
+
type: 'module-local'
|
|
2128
|
+
}
|
|
2104
2129
|
}
|
|
2105
2130
|
}
|
|
2106
2131
|
}
|