@stream44.studio/encapsulate 0.2.0-rc.2 → 0.2.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @stream44.studio/encapsulate might be problematic. Click here for more details.
- package/LICENSE.md +4 -199
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +81 -72
- package/src/encapsulate.ts +175 -25
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +207 -135
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +131 -37
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +53 -6
- package/src/static-analyzer.v0.ts +291 -52
|
@@ -1,90 +1,5 @@
|
|
|
1
1
|
import { CapsulePropertyTypes } from "../../encapsulate"
|
|
2
|
-
import { ContractCapsuleInstanceFactory } from "./Static.v0"
|
|
3
|
-
|
|
4
|
-
function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, filepath?: string, line?: number, column?: number }> {
|
|
5
|
-
const lines = stack.split('\n')
|
|
6
|
-
const result: Array<{ function?: string, filepath?: string, line?: number, column?: number }> = []
|
|
7
|
-
|
|
8
|
-
// Skip first line (Error message), then collect ALL frames
|
|
9
|
-
for (let i = 1; i < lines.length; i++) {
|
|
10
|
-
const line = lines[i].trim()
|
|
11
|
-
|
|
12
|
-
// Match various stack trace formats:
|
|
13
|
-
// "at functionName (/path/to/file:line:column)"
|
|
14
|
-
// "at /path/to/file:line:column"
|
|
15
|
-
// "at functionName (file:line:column)"
|
|
16
|
-
const match = line.match(/at\s+(.+)/)
|
|
17
|
-
if (match) {
|
|
18
|
-
const frame: { function?: string, filepath?: string, line?: number, column?: number } = {}
|
|
19
|
-
const content = match[1]
|
|
20
|
-
|
|
21
|
-
// Try to extract function name and location
|
|
22
|
-
const funcMatch = content.match(/^(.+?)\s+\((.+)\)$/)
|
|
23
|
-
if (funcMatch) {
|
|
24
|
-
// Has function name: "functionName (/path/to/file:line:column)"
|
|
25
|
-
const funcName = funcMatch[1].trim()
|
|
26
|
-
// Only include function name if not anonymous
|
|
27
|
-
if (funcName !== '<anonymous>' && funcName !== 'async <anonymous>') {
|
|
28
|
-
frame.function = funcName
|
|
29
|
-
}
|
|
30
|
-
const location = funcMatch[2]
|
|
31
|
-
const locMatch = location.match(/^(.+):(\d+):(\d+)$/)
|
|
32
|
-
if (locMatch) {
|
|
33
|
-
frame.filepath = locMatch[1]
|
|
34
|
-
frame.line = parseInt(locMatch[2], 10)
|
|
35
|
-
frame.column = parseInt(locMatch[3], 10)
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
// No function name: "/path/to/file:line:column"
|
|
39
|
-
const locMatch = content.match(/^(.+):(\d+):(\d+)$/)
|
|
40
|
-
if (locMatch) {
|
|
41
|
-
frame.filepath = locMatch[1]
|
|
42
|
-
frame.line = parseInt(locMatch[2], 10)
|
|
43
|
-
frame.column = parseInt(locMatch[3], 10)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Convert absolute paths to relative paths if spineFilesystemRoot is provided
|
|
48
|
-
if (frame.filepath && spineFilesystemRoot) {
|
|
49
|
-
if (frame.filepath.startsWith(spineFilesystemRoot)) {
|
|
50
|
-
frame.filepath = frame.filepath.slice(spineFilesystemRoot.length)
|
|
51
|
-
// Remove leading slash if present
|
|
52
|
-
if (frame.filepath.startsWith('/')) {
|
|
53
|
-
frame.filepath = frame.filepath.slice(1)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Include all frames, even if incomplete
|
|
59
|
-
if (frame.filepath || frame.function) {
|
|
60
|
-
result.push(frame)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return result
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function extractCallerInfo(stack: Array<{ function?: string, filepath?: string, line?: number, column?: number }>, offset: number = 0) {
|
|
68
|
-
// Use offset to skip frames in the stack
|
|
69
|
-
// offset 0 = first frame, offset 1 = second frame, etc.
|
|
70
|
-
|
|
71
|
-
if (offset < stack.length) {
|
|
72
|
-
const frame = stack[offset]
|
|
73
|
-
return {
|
|
74
|
-
filepath: frame.filepath,
|
|
75
|
-
line: frame.line
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Fallback to first frame if offset is out of bounds
|
|
80
|
-
if (stack.length > 0) {
|
|
81
|
-
return {
|
|
82
|
-
filepath: stack[0].filepath,
|
|
83
|
-
line: stack[0].line
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return {}
|
|
87
|
-
}
|
|
2
|
+
import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static.v0"
|
|
88
3
|
|
|
89
4
|
type CallerContext = {
|
|
90
5
|
capsuleSourceLineRef: string
|
|
@@ -144,13 +59,14 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
144
59
|
private encapsulateOptions: any
|
|
145
60
|
private capsuleSourceNameRef?: string
|
|
146
61
|
private capsuleSourceNameRefHash?: string
|
|
147
|
-
|
|
62
|
+
protected override runtimeSpineContracts?: Record<string, any>
|
|
148
63
|
public id: string
|
|
149
64
|
|
|
150
65
|
constructor({
|
|
151
66
|
spineContractUri,
|
|
152
67
|
capsule,
|
|
153
68
|
self,
|
|
69
|
+
ownSelf,
|
|
154
70
|
encapsulatedApi,
|
|
155
71
|
resolve,
|
|
156
72
|
importCapsule,
|
|
@@ -162,11 +78,14 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
162
78
|
getEventIndex,
|
|
163
79
|
incrementEventIndex,
|
|
164
80
|
currentCallerContext,
|
|
165
|
-
runtimeSpineContracts
|
|
81
|
+
runtimeSpineContracts,
|
|
82
|
+
instanceRegistry,
|
|
83
|
+
extendedCapsuleInstance
|
|
166
84
|
}: {
|
|
167
85
|
spineContractUri: string
|
|
168
86
|
capsule: any
|
|
169
87
|
self: any
|
|
88
|
+
ownSelf?: any
|
|
170
89
|
encapsulatedApi: Record<string, any>
|
|
171
90
|
resolve?: (uri: string, parentFilepath: string) => Promise<string>
|
|
172
91
|
importCapsule?: (filepath: string) => Promise<any>
|
|
@@ -179,8 +98,10 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
179
98
|
incrementEventIndex: () => number
|
|
180
99
|
currentCallerContext?: CallerContext
|
|
181
100
|
runtimeSpineContracts?: Record<string, any>
|
|
101
|
+
instanceRegistry?: CapsuleInstanceRegistry
|
|
102
|
+
extendedCapsuleInstance?: any
|
|
182
103
|
}) {
|
|
183
|
-
super({ spineContractUri, capsule, self, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule })
|
|
104
|
+
super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance })
|
|
184
105
|
this.getEventIndex = getEventIndex
|
|
185
106
|
this.incrementEventIndex = incrementEventIndex
|
|
186
107
|
this.currentCallerContext = currentCallerContext
|
|
@@ -202,9 +123,84 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
202
123
|
const mappedCapsule = await this.resolveMappedCapsule({ property })
|
|
203
124
|
const constants = await this.extractConstants({ mappedCapsule })
|
|
204
125
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
126
|
+
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
127
|
+
// options can be a function or an object for regular mappings
|
|
128
|
+
const mappingOptions = property.definition.delegateOptions
|
|
129
|
+
|| (typeof property.definition.options === 'function'
|
|
130
|
+
? await property.definition.options({ constants })
|
|
131
|
+
: property.definition.options)
|
|
132
|
+
|
|
133
|
+
// Check for existing instance in registry - reuse if available (regardless of options)
|
|
134
|
+
// Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
|
|
135
|
+
const capsuleName = mappedCapsule.encapsulateOptions?.capsuleName
|
|
136
|
+
|
|
137
|
+
if (capsuleName && this.instanceRegistry) {
|
|
138
|
+
if (this.instanceRegistry.has(capsuleName)) {
|
|
139
|
+
const existingEntry = this.instanceRegistry.get(capsuleName)
|
|
140
|
+
|
|
141
|
+
// If entry is null (pre-registered) or actual instance, and current mapping has no options, use deferred proxy
|
|
142
|
+
if (!mappingOptions) {
|
|
143
|
+
// Use deferred proxy that resolves from registry when accessed
|
|
144
|
+
const apiTarget = this.getApiTarget({ property })
|
|
145
|
+
const registry = this.instanceRegistry
|
|
146
|
+
apiTarget[property.name] = new Proxy({} as any, {
|
|
147
|
+
get: (_target: any, apiProp: string | symbol) => {
|
|
148
|
+
if (typeof apiProp === 'symbol') return undefined
|
|
149
|
+
const resolvedInstance = registry.get(capsuleName)
|
|
150
|
+
if (!resolvedInstance) {
|
|
151
|
+
throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.currentCallerContext = {
|
|
155
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
156
|
+
capsuleSourceNameRef: this.capsuleSourceNameRef,
|
|
157
|
+
spineContractCapsuleInstanceId: this.id,
|
|
158
|
+
capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
|
|
159
|
+
prop: apiProp as string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this.enableCallerStackInference) {
|
|
163
|
+
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
164
|
+
if (stackStr) {
|
|
165
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
166
|
+
if (stackFrames.length > 0) {
|
|
167
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
168
|
+
this.currentCallerContext.filepath = callerInfo.filepath
|
|
169
|
+
this.currentCallerContext.line = callerInfo.line
|
|
170
|
+
this.currentCallerContext.stack = stackFrames
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Access through .api if it exists (for capsule instances with getters)
|
|
176
|
+
if (resolvedInstance.api && apiProp in resolvedInstance.api) {
|
|
177
|
+
return resolvedInstance.api[apiProp]
|
|
178
|
+
}
|
|
179
|
+
return resolvedInstance[apiProp]
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
this.self[property.name] = new Proxy({} as any, {
|
|
183
|
+
get: (_target, prop) => {
|
|
184
|
+
if (typeof prop === 'symbol') return undefined
|
|
185
|
+
const resolvedInstance = registry.get(capsuleName)
|
|
186
|
+
if (!resolvedInstance) {
|
|
187
|
+
throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
|
|
188
|
+
}
|
|
189
|
+
const value = resolvedInstance.api?.[prop]
|
|
190
|
+
if (value && typeof value === 'object' && value.api) {
|
|
191
|
+
return value.api
|
|
192
|
+
}
|
|
193
|
+
return value
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
// If current mapping has options, fall through to create new instance
|
|
199
|
+
} else {
|
|
200
|
+
// Pre-register as null to claim the slot for this capsule
|
|
201
|
+
this.instanceRegistry.set(capsuleName, null)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
208
204
|
|
|
209
205
|
// Transform overrides if this mapping has a propertyContractDelegate
|
|
210
206
|
let mappedOverrides = overrides
|
|
@@ -237,6 +233,12 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
237
233
|
runtimeSpineContracts: this.runtimeSpineContracts
|
|
238
234
|
})
|
|
239
235
|
|
|
236
|
+
// Register the instance (replaces null pre-registration marker)
|
|
237
|
+
// Always register to make instance available for child capsules with deferred proxies
|
|
238
|
+
if (capsuleName && this.instanceRegistry) {
|
|
239
|
+
this.instanceRegistry.set(capsuleName, mappedCapsuleInstance)
|
|
240
|
+
}
|
|
241
|
+
|
|
240
242
|
const apiTarget = this.getApiTarget({ property })
|
|
241
243
|
apiTarget[property.name] = new Proxy(mappedCapsuleInstance, {
|
|
242
244
|
get: (apiTarget: any, apiProp: string | symbol) => {
|
|
@@ -263,6 +265,10 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
// Access through .api if it exists (for capsule instances with getters)
|
|
269
|
+
if (apiTarget.api && apiProp in apiTarget.api) {
|
|
270
|
+
return apiTarget.api[apiProp]
|
|
271
|
+
}
|
|
266
272
|
return apiTarget[apiProp]
|
|
267
273
|
}
|
|
268
274
|
})
|
|
@@ -402,27 +408,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
402
408
|
}
|
|
403
409
|
|
|
404
410
|
protected mapFunctionProperty({ property }: { property: any }) {
|
|
405
|
-
|
|
406
|
-
// Create a proxy for this.self that intercepts property access
|
|
407
|
-
// Prefer this.self (which has unwrapped APIs) over encapsulatedApi
|
|
408
|
-
const selfProxy = new Proxy(this.self, {
|
|
409
|
-
get: (target: any, prop: string | symbol) => {
|
|
410
|
-
if (typeof prop === 'symbol') return target[prop]
|
|
411
|
-
|
|
412
|
-
// First check if the property exists in target (this.self)
|
|
413
|
-
if (prop in target) {
|
|
414
|
-
return target[prop]
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Fall back to encapsulatedApi
|
|
418
|
-
if (prop in this.encapsulatedApi) {
|
|
419
|
-
return this.encapsulatedApi[prop]
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return undefined
|
|
423
|
-
}
|
|
424
|
-
})
|
|
425
|
-
|
|
411
|
+
const selfProxy = this.createSelfProxy()
|
|
426
412
|
const boundFunction = property.definition.value.bind(selfProxy)
|
|
427
413
|
|
|
428
414
|
const valueKey = `__value_${property.name}`
|
|
@@ -481,26 +467,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
481
467
|
|
|
482
468
|
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
483
469
|
const getterFn = property.definition.value
|
|
484
|
-
|
|
485
|
-
// Create a proxy for this.self that intercepts property access
|
|
486
|
-
// Prefer this.self (which has unwrapped APIs) over encapsulatedApi
|
|
487
|
-
const selfProxy = new Proxy(this.self, {
|
|
488
|
-
get: (target: any, prop: string | symbol) => {
|
|
489
|
-
if (typeof prop === 'symbol') return target[prop]
|
|
490
|
-
|
|
491
|
-
// First check if the property exists in target (this.self)
|
|
492
|
-
if (prop in target) {
|
|
493
|
-
return target[prop]
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Fall back to encapsulatedApi
|
|
497
|
-
if (prop in this.encapsulatedApi) {
|
|
498
|
-
return this.encapsulatedApi[prop]
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return undefined
|
|
502
|
-
}
|
|
503
|
-
})
|
|
470
|
+
const selfProxy = this.createSelfProxy()
|
|
504
471
|
|
|
505
472
|
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
506
473
|
get: () => {
|
|
@@ -532,6 +499,18 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
532
499
|
enumerable: true,
|
|
533
500
|
configurable: true
|
|
534
501
|
})
|
|
502
|
+
|
|
503
|
+
// Also define the getter on ownSelf so this.self.propertyName works for getter functions
|
|
504
|
+
// This ensures this.self accesses the getter, not a raw value
|
|
505
|
+
if (this.ownSelf) {
|
|
506
|
+
Object.defineProperty(this.ownSelf, property.name, {
|
|
507
|
+
get: () => {
|
|
508
|
+
return getterFn.call(selfProxy)
|
|
509
|
+
},
|
|
510
|
+
enumerable: true,
|
|
511
|
+
configurable: true
|
|
512
|
+
})
|
|
513
|
+
}
|
|
535
514
|
}
|
|
536
515
|
|
|
537
516
|
private addCallerContextToEvent(event: any): void {
|
|
@@ -592,14 +571,17 @@ export function CapsuleSpineContract({
|
|
|
592
571
|
|
|
593
572
|
let eventIndex = 0
|
|
594
573
|
let currentCallerContext: CallerContext | undefined = undefined
|
|
574
|
+
const instanceRegistry: CapsuleInstanceRegistry = new Map()
|
|
595
575
|
|
|
596
576
|
return {
|
|
597
577
|
'#': CapsuleSpineContract['#'],
|
|
598
|
-
|
|
578
|
+
instanceRegistry,
|
|
579
|
+
makeContractCapsuleInstance: ({ encapsulateOptions, spineContractUri, self, ownSelf, capsule, encapsulatedApi, runtimeSpineContracts, extendedCapsuleInstance }: { encapsulateOptions: any, spineContractUri: string, self: any, ownSelf?: any, capsule?: any, encapsulatedApi: Record<string, any>, runtimeSpineContracts?: Record<string, any>, extendedCapsuleInstance?: any }) => {
|
|
599
580
|
return new MembraneContractCapsuleInstanceFactory({
|
|
600
581
|
spineContractUri,
|
|
601
582
|
capsule,
|
|
602
583
|
self,
|
|
584
|
+
ownSelf,
|
|
603
585
|
encapsulatedApi,
|
|
604
586
|
spineFilesystemRoot,
|
|
605
587
|
freezeCapsule,
|
|
@@ -611,7 +593,9 @@ export function CapsuleSpineContract({
|
|
|
611
593
|
getEventIndex: () => eventIndex,
|
|
612
594
|
incrementEventIndex: () => eventIndex++,
|
|
613
595
|
currentCallerContext,
|
|
614
|
-
runtimeSpineContracts
|
|
596
|
+
runtimeSpineContracts,
|
|
597
|
+
instanceRegistry,
|
|
598
|
+
extendedCapsuleInstance
|
|
615
599
|
})
|
|
616
600
|
},
|
|
617
601
|
hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
|
|
@@ -622,3 +606,91 @@ export function CapsuleSpineContract({
|
|
|
622
606
|
|
|
623
607
|
CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'
|
|
624
608
|
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, filepath?: string, line?: number, column?: number }> {
|
|
613
|
+
const lines = stack.split('\n')
|
|
614
|
+
const result: Array<{ function?: string, filepath?: string, line?: number, column?: number }> = []
|
|
615
|
+
|
|
616
|
+
// Skip first line (Error message), then collect ALL frames
|
|
617
|
+
for (let i = 1; i < lines.length; i++) {
|
|
618
|
+
const line = lines[i].trim()
|
|
619
|
+
|
|
620
|
+
// Match various stack trace formats:
|
|
621
|
+
// "at functionName (/path/to/file:line:column)"
|
|
622
|
+
// "at /path/to/file:line:column"
|
|
623
|
+
// "at functionName (file:line:column)"
|
|
624
|
+
const match = line.match(/at\s+(.+)/)
|
|
625
|
+
if (match) {
|
|
626
|
+
const frame: { function?: string, filepath?: string, line?: number, column?: number } = {}
|
|
627
|
+
const content = match[1]
|
|
628
|
+
|
|
629
|
+
// Try to extract function name and location
|
|
630
|
+
const funcMatch = content.match(/^(.+?)\s+\((.+)\)$/)
|
|
631
|
+
if (funcMatch) {
|
|
632
|
+
// Has function name: "functionName (/path/to/file:line:column)"
|
|
633
|
+
const funcName = funcMatch[1].trim()
|
|
634
|
+
// Only include function name if not anonymous
|
|
635
|
+
if (funcName !== '<anonymous>' && funcName !== 'async <anonymous>') {
|
|
636
|
+
frame.function = funcName
|
|
637
|
+
}
|
|
638
|
+
const location = funcMatch[2]
|
|
639
|
+
const locMatch = location.match(/^(.+):(\d+):(\d+)$/)
|
|
640
|
+
if (locMatch) {
|
|
641
|
+
frame.filepath = locMatch[1]
|
|
642
|
+
frame.line = parseInt(locMatch[2], 10)
|
|
643
|
+
frame.column = parseInt(locMatch[3], 10)
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
// No function name: "/path/to/file:line:column"
|
|
647
|
+
const locMatch = content.match(/^(.+):(\d+):(\d+)$/)
|
|
648
|
+
if (locMatch) {
|
|
649
|
+
frame.filepath = locMatch[1]
|
|
650
|
+
frame.line = parseInt(locMatch[2], 10)
|
|
651
|
+
frame.column = parseInt(locMatch[3], 10)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Convert absolute paths to relative paths if spineFilesystemRoot is provided
|
|
656
|
+
if (frame.filepath && spineFilesystemRoot) {
|
|
657
|
+
if (frame.filepath.startsWith(spineFilesystemRoot)) {
|
|
658
|
+
frame.filepath = frame.filepath.slice(spineFilesystemRoot.length)
|
|
659
|
+
// Remove leading slash if present
|
|
660
|
+
if (frame.filepath.startsWith('/')) {
|
|
661
|
+
frame.filepath = frame.filepath.slice(1)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Include all frames, even if incomplete
|
|
667
|
+
if (frame.filepath || frame.function) {
|
|
668
|
+
result.push(frame)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return result
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function extractCallerInfo(stack: Array<{ function?: string, filepath?: string, line?: number, column?: number }>, offset: number = 0) {
|
|
676
|
+
// Use offset to skip frames in the stack
|
|
677
|
+
// offset 0 = first frame, offset 1 = second frame, etc.
|
|
678
|
+
|
|
679
|
+
if (offset < stack.length) {
|
|
680
|
+
const frame = stack[offset]
|
|
681
|
+
return {
|
|
682
|
+
filepath: frame.filepath,
|
|
683
|
+
line: frame.line
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Fallback to first frame if offset is out of bounds
|
|
688
|
+
if (stack.length > 0) {
|
|
689
|
+
return {
|
|
690
|
+
filepath: stack[0].filepath,
|
|
691
|
+
line: stack[0].line
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return {}
|
|
695
|
+
}
|
|
696
|
+
|