@stream44.studio/encapsulate 0.4.0-rc.28 → 0.4.0-rc.29

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream44.studio/encapsulate",
3
- "version": "0.4.0-rc.28",
3
+ "version": "0.4.0-rc.29",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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?.moduleFilepath
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?.moduleFilepath
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 moduleFilepath is referenced in any mapping property
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 moduleFilepath = targetCapsule.cst.source.moduleFilepath
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: '${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
  }
@@ -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 = 21
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
- const capsuleSourceLineRef = `${moduleFilepath}:${importStackLine}`
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 { csts, crts } = await spine.spineOptions.staticAnalyzer?.parseModule({
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
  }
@@ -140,11 +140,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
140
140
 
141
141
  // delegateOptions is set by encapsulate.ts for property contract delegates
142
142
  // options can be a function or an object for regular mappings
143
- // Always pass { self, constants } - self is populated when depends is specified, empty otherwise
143
+ // Always pass { self, constants } - self contains full parent self when depends is specified,
144
+ // otherwise just the Capsule metadata struct (moduleFilepath, capsuleName, etc.)
144
145
  const optionsFn = property.definition.options
146
+ const capsuleStructKey = '#@stream44.studio/encapsulate/structs/Capsule'
147
+ const minimalSelf = this.self[capsuleStructKey]
148
+ ? { [capsuleStructKey]: this.self[capsuleStructKey] }
149
+ : {}
145
150
  const mappingOptions = property.definition.delegateOptions
146
151
  || (typeof optionsFn === 'function'
147
- ? await optionsFn({ self: property.definition.depends ? this.self : {}, constants })
152
+ ? await optionsFn({ self: property.definition.depends ? this.self : minimalSelf, constants })
148
153
  : optionsFn)
149
154
 
150
155
  // Check for existing instance in registry - reuse if available (regardless of options)
@@ -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 parent capsule's `self` object with resolved sibling mappings. Only populated when `depends` is specified (empty `{}` otherwise).
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`) and the Capsule metadata struct (e.g. `self['#@stream44.studio/encapsulate/structs/Capsule'].capsuleName`). Can be declared explicitly or auto-injected by the static analyzer when it detects `self.<name>` references in the options function body.
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`).
@@ -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 encapsulateOptions.moduleFilepath (always available) instead of cst.source.moduleFilepath
133
- const moduleFilepath = this.capsule.encapsulateOptions?.moduleFilepath || this.capsule.cst?.source?.moduleFilepath
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 is populated when depends is specified, empty otherwise
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 : {}, constants })
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
- // Determine the cache file path based on whether the module is external or internal
248
- let cacheFilePath: string
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
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
- const capsuleSourceLineRef = `${encapsulateOptions.moduleFilepath}:${encapsulateOptions.importStackLine}`
373
- const capsuleSourceNameRef = encapsulateOptions.capsuleName && `${encapsulateOptions.moduleFilepath}:${encapsulateOptions.capsuleName}`
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
- // Construct npm URI for the module - try for all modules
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
  }