@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 +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +12 -12
- package/src/encapsulate.ts +14 -5
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +7 -2
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +2 -2
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +10 -4
- package/src/static-analyzer.v0.ts +15 -37
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
|
}
|
|
@@ -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
|
|
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 :
|
|
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
|
|
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`).
|
|
@@ -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
|
}
|