@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 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
  ![Capsule Spine Contract Overview](./src/spine-contracts/CapsuleSpineContract.v0/Overview.svg)
46
48
 
47
-
48
49
  Provenance
49
50
  ===
50
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream44.studio/encapsulate",
3
- "version": "0.4.0-rc.22",
3
+ "version": "0.4.0-rc.24",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- if (moduleFilepath.startsWith('../')) {
32
- // External module - construct npm URI
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
@@ -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 = 18
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 currentCallerContext: CallerContext | undefined
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
- currentCallerContext,
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
- currentCallerContext?: CallerContext
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.currentCallerContext = currentCallerContext
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
- setCurrentCallerContext(context: CallerContext | undefined): void {
120
- this.currentCallerContext = context
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
- this.currentCallerContext = {
159
- capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
160
- capsuleSourceNameRef: this.capsuleSourceNameRef,
161
- spineContractCapsuleInstanceId: this.id,
162
- capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
163
- prop: apiProp as string
164
- }
165
-
166
- if (this.enableCallerStackInference) {
167
- const stackStr = new Error('[MAPPED_CAPSULE]').stack
168
- if (stackStr) {
169
- const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
170
- if (stackFrames.length > 0) {
171
- const callerInfo = extractCallerInfo(stackFrames, 3)
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
- this.currentCallerContext = {
284
- capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
285
- capsuleSourceNameRef: this.capsuleSourceNameRef,
286
- spineContractCapsuleInstanceId: this.id,
287
- capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
288
- prop: apiProp as string
289
- }
290
-
291
- if (this.enableCallerStackInference) {
292
- const stackStr = new Error('[MAPPED_CAPSULE]').stack
293
- if (stackStr) {
294
- const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
295
- if (stackFrames.length > 0) {
296
- const callerInfo = extractCallerInfo(stackFrames, 3)
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
- this.currentCallerContext = {
349
- capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
350
- capsuleSourceNameRef: this.capsuleSourceNameRef,
351
- spineContractCapsuleInstanceId: this.id,
352
- capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
353
- prop: key
354
- }
355
-
356
- if (this.enableCallerStackInference) {
357
- const stackStr = new Error('[PROPERTY_CONTRACT_DELEGATE]').stack
358
- if (stackStr) {
359
- const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
360
- if (stackFrames.length > 0) {
361
- const callerInfo = extractCallerInfo(stackFrames, 3)
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
- // Call the getter function lazily when accessed with proper this context
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
- if (this.currentCallerContext) {
716
+ const callerCtx = this.getCurrentCallerContext()
717
+ if (callerCtx) {
697
718
  event.caller = {
698
- capsuleSourceLineRef: this.currentCallerContext.capsuleSourceLineRef,
699
- spineContractCapsuleInstanceId: this.currentCallerContext.spineContractCapsuleInstanceId,
719
+ capsuleSourceLineRef: callerCtx.capsuleSourceLineRef,
720
+ spineContractCapsuleInstanceId: callerCtx.spineContractCapsuleInstanceId,
721
+ }
722
+ if (callerCtx.capsuleSourceNameRef) {
723
+ event.caller.capsuleSourceNameRef = callerCtx.capsuleSourceNameRef
700
724
  }
701
- if (this.currentCallerContext.capsuleSourceNameRef) {
702
- event.caller.capsuleSourceNameRef = this.currentCallerContext.capsuleSourceNameRef
725
+ if (callerCtx.capsuleSourceNameRefHash) {
726
+ event.caller.capsuleSourceNameRefHash = callerCtx.capsuleSourceNameRefHash
703
727
  }
704
- if (this.currentCallerContext.capsuleSourceNameRefHash) {
705
- event.caller.capsuleSourceNameRefHash = this.currentCallerContext.capsuleSourceNameRefHash
728
+ if (callerCtx.capsuleSourceUriLineRefInstanceId) {
729
+ event.caller.capsuleSourceUriLineRefInstanceId = callerCtx.capsuleSourceUriLineRefInstanceId
706
730
  }
707
- if (this.currentCallerContext.prop) {
708
- event.caller.prop = this.currentCallerContext.prop
731
+ if (callerCtx.prop) {
732
+ event.caller.prop = callerCtx.prop
709
733
  }
710
- if (this.currentCallerContext.filepath) {
711
- event.caller.filepath = this.currentCallerContext.filepath
734
+ if (callerCtx.filepath) {
735
+ event.caller.filepath = callerCtx.filepath
712
736
  }
713
- if (this.currentCallerContext.line) {
714
- event.caller.line = this.currentCallerContext.line
737
+ if (callerCtx.line) {
738
+ event.caller.line = callerCtx.line
715
739
  }
716
- if (this.currentCallerContext.stack) {
717
- event.caller.stack = this.currentCallerContext.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 this.encapsulatedApi) {
388
- return this.encapsulatedApi[prop]
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
- if (encapsulateOptions.moduleFilepath.startsWith('../')) {
245
- // External module - construct npm URI
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('md5').update(capsuleSourceNameRef).digest('hex')
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[propName]) {
531
- spineContractDef.propertyContracts[propName] = {
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' property at the property contract level
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) && contractProp.name.text === 'as') {
540
- if (ts.isStringLiteral(contractProp.initializer)) {
541
- spineContractDef.propertyContracts[propName].as = contractProp.initializer.text
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[propName].properties[contractPropName] = propDef
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
package/tsconfig.json CHANGED
@@ -1,5 +1,4 @@
1
1
  {
2
- "extends": "../../../tsconfig.paths.json",
3
2
  "compilerOptions": {
4
3
  "target": "ES2020",
5
4
  "module": "esnext",
@@ -27,4 +26,4 @@
27
26
  "exclude": [
28
27
  "node_modules"
29
28
  ]
30
- }
29
+ }