@stream44.studio/encapsulate 0.4.0-rc.32 → 0.4.0-rc.38
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 +3 -2
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +51 -446
- package/src/encapsulate.ts +123 -18
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +29 -69
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +27 -12
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +84 -2
- package/structs/CapsuleProjectionContext.ts +53 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream44.studio/encapsulate",
|
|
3
|
-
"version": "0.4.0-rc.
|
|
3
|
+
"version": "0.4.0-rc.38",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"./spine-contracts/CapsuleSpineContract.v0/Membrane.v0": "./src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts",
|
|
15
15
|
"./spine-factories/CapsuleSpineFactory.v0": "./src/spine-factories/CapsuleSpineFactory.v0.ts",
|
|
16
16
|
"./spine-factories/TimingObserver": "./src/spine-factories/TimingObserver.ts",
|
|
17
|
-
"./structs/Capsule": "./structs/Capsule.ts"
|
|
17
|
+
"./structs/Capsule": "./structs/Capsule.ts",
|
|
18
|
+
"./structs/CapsuleProjectionContext": "./structs/CapsuleProjectionContext.ts"
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
20
21
|
"test": "bun test",
|
|
@@ -199,6 +199,9 @@ export function CapsuleModuleProjector({
|
|
|
199
199
|
for (const nestedPropName in propContract.properties) {
|
|
200
200
|
if (nestedPropName.startsWith('/')) {
|
|
201
201
|
const nestedProp = propContract.properties[nestedPropName]
|
|
202
|
+
// Skip if this mapping has a propertyContractDelegate — the delegate
|
|
203
|
+
// handles its own projection via OnFreeze
|
|
204
|
+
if (nestedProp.propertyContractDelegate) continue
|
|
202
205
|
// Check if this is a Mapping type property
|
|
203
206
|
if (nestedProp.type === 'CapsulePropertyTypes.Mapping') {
|
|
204
207
|
// First try to find the mapped capsule in ambient references
|
|
@@ -287,23 +290,6 @@ export function CapsuleModuleProjector({
|
|
|
287
290
|
return uris
|
|
288
291
|
}
|
|
289
292
|
|
|
290
|
-
// Helper: Check if capsule has solidjs.com/standalone property
|
|
291
|
-
function hasSolidJsProperty(capsule: any, spineContractUri: string): boolean {
|
|
292
|
-
const spineContract = capsule.cst.spineContracts[spineContractUri]
|
|
293
|
-
// Check both top-level and nested under '#' property contract
|
|
294
|
-
const topLevelProps = spineContract?.propertyContracts || {}
|
|
295
|
-
const nestedProps = spineContract?.propertyContracts?.['#']?.properties || {}
|
|
296
|
-
|
|
297
|
-
// Check for solidjs.com/standalone specifically
|
|
298
|
-
for (const key of Object.keys(topLevelProps)) {
|
|
299
|
-
if (key === 'solidjs.com/standalone') return true
|
|
300
|
-
}
|
|
301
|
-
for (const key of Object.keys(nestedProps)) {
|
|
302
|
-
if (key === 'solidjs.com/standalone') return true
|
|
303
|
-
}
|
|
304
|
-
return false
|
|
305
|
-
}
|
|
306
|
-
|
|
307
293
|
// Helper: Check if capsule has encapsulate.dev/standalone property (with optional suffix)
|
|
308
294
|
function hasStandaloneProperty(capsule: any, spineContractUri: string): boolean {
|
|
309
295
|
const spineContract = capsule.cst.spineContracts[spineContractUri]
|
|
@@ -321,66 +307,6 @@ export function CapsuleModuleProjector({
|
|
|
321
307
|
return false
|
|
322
308
|
}
|
|
323
309
|
|
|
324
|
-
// Helper: Extract SolidJS component function from capsule definition
|
|
325
|
-
function extractSolidJsComponent(capsule: any, spineContractUri: string): string | null {
|
|
326
|
-
const spineContract = capsule.cst.spineContracts[spineContractUri]
|
|
327
|
-
|
|
328
|
-
// Check nested under '#' property contract first, looking for solidjs.com/standalone
|
|
329
|
-
const nestedProps = spineContract?.propertyContracts?.['#']?.properties || {}
|
|
330
|
-
const topLevelProps = spineContract?.propertyContracts || {}
|
|
331
|
-
|
|
332
|
-
let solidjsProp = null
|
|
333
|
-
for (const key of Object.keys(nestedProps)) {
|
|
334
|
-
if (key === 'solidjs.com/standalone') {
|
|
335
|
-
solidjsProp = nestedProps[key]
|
|
336
|
-
break
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (!solidjsProp) {
|
|
340
|
-
for (const key of Object.keys(topLevelProps)) {
|
|
341
|
-
if (key === 'solidjs.com/standalone') {
|
|
342
|
-
solidjsProp = topLevelProps[key]
|
|
343
|
-
break
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (!solidjsProp || solidjsProp.type !== 'CapsulePropertyTypes.Function') {
|
|
349
|
-
return null
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Extract the value expression which contains the component function
|
|
353
|
-
const valueExpression = solidjsProp.valueExpression
|
|
354
|
-
if (!valueExpression) return null
|
|
355
|
-
|
|
356
|
-
// The value expression is: "function (this: any): Function {\n return function ComponentName() { ... }\n}"
|
|
357
|
-
// We need to extract the inner function after "return "
|
|
358
|
-
// Use a more flexible regex that handles multiline and varying whitespace
|
|
359
|
-
const match = valueExpression.match(/return\s+(function\s+\w*\s*\([^)]*\)\s*\{[\s\S]*)\s*\}\s*$/m)
|
|
360
|
-
if (match) {
|
|
361
|
-
// Clean up the extracted function - remove extra indentation
|
|
362
|
-
let extracted = match[1].trim()
|
|
363
|
-
// Add back the closing brace if it was removed
|
|
364
|
-
if (!extracted.endsWith('}')) {
|
|
365
|
-
extracted += '\n}'
|
|
366
|
-
}
|
|
367
|
-
// Remove leading indentation from each line
|
|
368
|
-
const lines = extracted.split('\n')
|
|
369
|
-
const minIndent = lines
|
|
370
|
-
.filter(line => line.trim().length > 0)
|
|
371
|
-
.map(line => line.match(/^(\s*)/)?.[1].length || 0)
|
|
372
|
-
.reduce((min, indent) => Math.min(min, indent), Infinity)
|
|
373
|
-
|
|
374
|
-
if (minIndent > 0 && minIndent !== Infinity) {
|
|
375
|
-
extracted = lines.map(line => line.substring(minIndent)).join('\n')
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return extracted
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return null
|
|
382
|
-
}
|
|
383
|
-
|
|
384
310
|
// Helper: Extract standalone function from capsule definition
|
|
385
311
|
function extractStandaloneFunction(capsule: any, spineContractUri: string): string | null {
|
|
386
312
|
const spineContract = capsule.cst.spineContracts[spineContractUri]
|
|
@@ -812,196 +738,15 @@ export function CapsuleModuleProjector({
|
|
|
812
738
|
|
|
813
739
|
const allStatements = [importStatements, literalReferences, moduleLocalFunctions].filter(Boolean).join('\n')
|
|
814
740
|
|
|
815
|
-
// Check if this capsule has
|
|
816
|
-
const hasSolidJs = hasSolidJsProperty(capsule, spineContractUri)
|
|
741
|
+
// Check if this capsule has an encapsulate.dev/standalone property
|
|
817
742
|
const hasStandalone = hasStandaloneProperty(capsule, spineContractUri)
|
|
818
|
-
const needsRuntime = hasSolidJs || hasStandalone
|
|
819
|
-
|
|
820
|
-
// Determine which solid-js imports are needed (avoid duplicates with ambient references)
|
|
821
|
-
const existingSolidJsImports = new Set<string>()
|
|
822
|
-
for (const [name, ref] of Object.entries(ambientReferences)) {
|
|
823
|
-
const refTyped = ref as any
|
|
824
|
-
if (refTyped.type === 'import' && refTyped.moduleUri === 'solid-js') {
|
|
825
|
-
// Parse existing imports from solid-js
|
|
826
|
-
const match = refTyped.importSpecifier?.match(/\{([^}]+)\}/)
|
|
827
|
-
if (match) {
|
|
828
|
-
match[1].split(',').forEach((imp: string) => existingSolidJsImports.add(imp.trim()))
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const neededSolidJsImports = ['createSignal', 'onMount', 'Show'].filter(imp => !existingSolidJsImports.has(imp))
|
|
834
|
-
const solidJsImport = hasSolidJs && neededSolidJsImports.length > 0 ? `import { ${neededSolidJsImports.join(', ')} } from 'solid-js'\n` : ''
|
|
835
743
|
|
|
836
|
-
// Add runtime imports for
|
|
837
|
-
const runtimeImport =
|
|
744
|
+
// Add runtime imports for standalone functions
|
|
745
|
+
const runtimeImport = hasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane.v0'\n` : ''
|
|
838
746
|
|
|
839
747
|
// Generate default export based on capsule type
|
|
840
748
|
let defaultExport = ''
|
|
841
|
-
if (
|
|
842
|
-
// Generate a wrapper that sets up runtime and exports the SolidJS component
|
|
843
|
-
const capsuleSourceLineRef = capsule.cst.capsuleSourceLineRef
|
|
844
|
-
const solidjsComponent = extractSolidJsComponent(capsule, spineContractUri)
|
|
845
|
-
if (solidjsComponent) {
|
|
846
|
-
// Collect all capsule URIs from CST (mappings and property contracts)
|
|
847
|
-
const allCapsuleUris = collectAllCapsuleUris(capsule, spineContractUri)
|
|
848
|
-
|
|
849
|
-
// Also collect from ambient references and build import paths from snapshots
|
|
850
|
-
const capsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
|
|
851
|
-
for (const [name, ref] of Object.entries(ambientReferences)) {
|
|
852
|
-
const refTyped = ref as any
|
|
853
|
-
if (refTyped.type === 'capsule') {
|
|
854
|
-
const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
|
|
855
|
-
|
|
856
|
-
// Use dynamic spineContractUri instead of hardcoded URI
|
|
857
|
-
const contractData = snapshot.spineContracts?.[spineContractUri]
|
|
858
|
-
const structKey = Object.keys(contractData || {}).find(k => k.includes('/structs/Capsule'))
|
|
859
|
-
const capsuleName = structKey ? contractData[structKey]?.capsuleName : undefined
|
|
860
|
-
const projectedFilepath = structKey ? contractData[structKey]?.projectedCapsuleFilepath : undefined
|
|
861
|
-
|
|
862
|
-
if (capsuleName && projectedFilepath) {
|
|
863
|
-
allCapsuleUris.add(capsuleName)
|
|
864
|
-
|
|
865
|
-
// Build import path from projected filepath
|
|
866
|
-
const importName = `_capsule_${capsuleName.replace(/[^a-zA-Z0-9]/g, '_')}`
|
|
867
|
-
// Remove .~o/encapsulate.dev/caps/ prefix and strip extension
|
|
868
|
-
const importPath = projectedFilepath.replace(/^\.~o\/encapsulate\.dev\/caps\//, '').replace(/\.(ts|tsx)$/, '')
|
|
869
|
-
|
|
870
|
-
capsuleDeps.push({ uri: capsuleName, importName, importPath })
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Generate static imports for all capsule dependencies
|
|
876
|
-
// Compute relative path from projected file to caps directory
|
|
877
|
-
let importPrefix: string
|
|
878
|
-
if (capsuleModuleProjectionPackage) {
|
|
879
|
-
importPrefix = capsuleModuleProjectionPackage
|
|
880
|
-
} else {
|
|
881
|
-
const projectedFileDir = dirname(filepath)
|
|
882
|
-
const capsDir = '.~o/encapsulate.dev/caps'
|
|
883
|
-
const relativePathToCaps = relative(projectedFileDir, capsDir)
|
|
884
|
-
importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
|
|
885
|
-
}
|
|
886
|
-
const capsuleImports = capsuleDeps.map(dep =>
|
|
887
|
-
`import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
|
|
888
|
-
).join('\n')
|
|
889
|
-
|
|
890
|
-
// Generate capsules map
|
|
891
|
-
const capsulesMapEntries = capsuleDeps.map(dep =>
|
|
892
|
-
` '${dep.uri}': ${dep.importName}`
|
|
893
|
-
).join(',\n')
|
|
894
|
-
|
|
895
|
-
defaultExport = `
|
|
896
|
-
${capsuleImports}
|
|
897
|
-
|
|
898
|
-
// Set up runtime for browser execution
|
|
899
|
-
const sourceSpine: { encapsulate?: any } = {}
|
|
900
|
-
|
|
901
|
-
// Map of statically imported capsules
|
|
902
|
-
const capsulesMap: Record<string, any> = {
|
|
903
|
-
${capsulesMapEntries}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Helper to import and instantiate a capsule from the capsules map
|
|
907
|
-
const importCapsule = async (uri: string) => {
|
|
908
|
-
const capsuleModule = capsulesMap[uri]
|
|
909
|
-
if (!capsuleModule) {
|
|
910
|
-
throw new Error(\`Capsule not found in static imports: \${uri}\`)
|
|
911
|
-
}
|
|
912
|
-
const capsule = await capsuleModule.capsule({
|
|
913
|
-
encapsulate: sourceSpine.encapsulate,
|
|
914
|
-
loadCapsule
|
|
915
|
-
})
|
|
916
|
-
return capsule
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
|
|
920
|
-
// Return the capsule function from this projected file
|
|
921
|
-
if (capsuleSourceLineRef === '${capsuleSourceLineRef}') {
|
|
922
|
-
return capsule
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Use capsuleName directly if provided
|
|
926
|
-
if (capsuleName) {
|
|
927
|
-
return await importCapsule(capsuleName)
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const spineContractOpts = {
|
|
934
|
-
spineFilesystemRoot: '.',
|
|
935
|
-
resolve: async (uri: string) => uri,
|
|
936
|
-
importCapsule
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
const runtimeSpineContracts = {
|
|
940
|
-
['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
const snapshot = {
|
|
944
|
-
capsules: {
|
|
945
|
-
['${capsuleSourceLineRef}']: {
|
|
946
|
-
spineContracts: {}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Export wrapper function that initializes runtime and returns component
|
|
952
|
-
export default function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
|
|
953
|
-
const [component, setComponent] = createSignal(null)
|
|
954
|
-
|
|
955
|
-
onMount(async () => {
|
|
956
|
-
// Add onMembraneEvent to spine contract opts - use provided or default logger
|
|
957
|
-
const defaultMembraneLogger = (event: any) => {
|
|
958
|
-
console.log('[Membrane Event]', event)
|
|
959
|
-
}
|
|
960
|
-
const opts = {
|
|
961
|
-
...spineContractOpts,
|
|
962
|
-
onMembraneEvent: onMembraneEvent || defaultMembraneLogger
|
|
963
|
-
}
|
|
964
|
-
const contracts = {
|
|
965
|
-
['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(opts)
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const { encapsulate, capsules } = await Spine({
|
|
969
|
-
spineFilesystemRoot: '.',
|
|
970
|
-
spineContracts: contracts
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
sourceSpine.encapsulate = encapsulate
|
|
974
|
-
|
|
975
|
-
const capsuleInstance = await capsule({ encapsulate, loadCapsule })
|
|
976
|
-
|
|
977
|
-
const { run } = await SpineRuntime({
|
|
978
|
-
spineFilesystemRoot: '.',
|
|
979
|
-
spineContracts: contracts,
|
|
980
|
-
snapshot,
|
|
981
|
-
loadCapsule
|
|
982
|
-
})
|
|
983
|
-
|
|
984
|
-
const Component = await run({}, async ({ apis }) => {
|
|
985
|
-
const capsuleApi = apis['${capsuleSourceLineRef}']
|
|
986
|
-
const solidjsKey = Object.keys(capsuleApi).find(k => k === 'solidjs.com/standalone')
|
|
987
|
-
if (!solidjsKey) throw new Error('solidjs.com/standalone property not found')
|
|
988
|
-
return capsuleApi[solidjsKey]()
|
|
989
|
-
})
|
|
990
|
-
|
|
991
|
-
setComponent(() => Component)
|
|
992
|
-
})
|
|
993
|
-
|
|
994
|
-
// Return the wrapper function itself, not call it
|
|
995
|
-
const WrapperComponent = () => {
|
|
996
|
-
const Component = component()
|
|
997
|
-
return Show({ when: Component, children: (Component) => Component() })
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
return WrapperComponent
|
|
1001
|
-
}
|
|
1002
|
-
`
|
|
1003
|
-
}
|
|
1004
|
-
} else if (hasStandalone) {
|
|
749
|
+
if (hasStandalone) {
|
|
1005
750
|
// Generate a wrapper function that directly invokes the standalone function
|
|
1006
751
|
const capsuleSourceLineRef = capsule.cst.capsuleSourceLineRef
|
|
1007
752
|
|
|
@@ -1095,6 +840,7 @@ ${capsulesMapEntries}
|
|
|
1095
840
|
}
|
|
1096
841
|
const capsule = await capsuleModule.capsule({
|
|
1097
842
|
encapsulate: sourceSpine.encapsulate,
|
|
843
|
+
CapsulePropertyTypes,
|
|
1098
844
|
loadCapsule
|
|
1099
845
|
})
|
|
1100
846
|
return capsule
|
|
@@ -1255,8 +1001,12 @@ ${defaultExport}
|
|
|
1255
1001
|
mappedCapsule.cst.source.moduleFilepath
|
|
1256
1002
|
)
|
|
1257
1003
|
|
|
1258
|
-
// Get ambient references for the mapped capsule
|
|
1259
|
-
|
|
1004
|
+
// Get ambient references for the mapped capsule, excluding makeImportStack
|
|
1005
|
+
let mappedAmbientRefs = mappedCapsule.cst.source?.ambientReferences || {}
|
|
1006
|
+
if (mappedAmbientRefs['makeImportStack']) {
|
|
1007
|
+
mappedAmbientRefs = { ...mappedAmbientRefs }
|
|
1008
|
+
delete mappedAmbientRefs['makeImportStack']
|
|
1009
|
+
}
|
|
1260
1010
|
|
|
1261
1011
|
// Generate import statements with projection CSS paths
|
|
1262
1012
|
const mappedImportStatements = Object.entries(mappedAmbientRefs)
|
|
@@ -1315,191 +1065,17 @@ ${defaultExport}
|
|
|
1315
1065
|
|
|
1316
1066
|
const mappedAllStatements = [mappedImportStatements, mappedLiteralReferences, mappedModuleLocalFunctions].filter(Boolean).join('\n')
|
|
1317
1067
|
|
|
1318
|
-
// Check if mapped capsule has
|
|
1319
|
-
const mappedHasSolidJs = hasSolidJsProperty(mappedCapsule, spineContractUri)
|
|
1068
|
+
// Check if mapped capsule has encapsulate.dev/standalone property
|
|
1320
1069
|
const mappedHasStandalone = hasStandaloneProperty(mappedCapsule, spineContractUri)
|
|
1321
|
-
const mappedNeedsRuntime = mappedHasSolidJs || mappedHasStandalone
|
|
1322
|
-
|
|
1323
|
-
// Determine which solid-js imports are needed for mapped capsule
|
|
1324
|
-
const mappedExistingSolidJsImports = new Set<string>()
|
|
1325
|
-
for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
|
|
1326
|
-
const refTyped = ref as any
|
|
1327
|
-
if (refTyped.type === 'import' && refTyped.moduleUri === 'solid-js') {
|
|
1328
|
-
const match = refTyped.importSpecifier?.match(/\{([^}]+)\}/)
|
|
1329
|
-
if (match) {
|
|
1330
|
-
match[1].split(',').forEach((imp: string) => mappedExistingSolidJsImports.add(imp.trim()))
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
const mappedNeededSolidJsImports = ['createSignal', 'onMount', 'Show'].filter(imp => !mappedExistingSolidJsImports.has(imp))
|
|
1336
|
-
const mappedSolidJsImport = mappedHasSolidJs && mappedNeededSolidJsImports.length > 0 ? `import { ${mappedNeededSolidJsImports.join(', ')} } from 'solid-js'\n` : ''
|
|
1337
1070
|
|
|
1338
1071
|
// Rewrite the mapped capsule expression to include CST (reuse the same function)
|
|
1339
1072
|
const mappedCapsuleExpression = rewriteCapsuleExpressionWithCST(mappedCapsule)
|
|
1340
1073
|
|
|
1341
|
-
// Add runtime imports for
|
|
1342
|
-
const mappedRuntimeImport =
|
|
1074
|
+
// Add runtime imports for standalone functions
|
|
1075
|
+
const mappedRuntimeImport = mappedHasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane.v0'\n` : ''
|
|
1343
1076
|
|
|
1344
1077
|
let mappedDefaultExport = ''
|
|
1345
|
-
if (
|
|
1346
|
-
// Generate a wrapper that sets up runtime and exports the SolidJS component
|
|
1347
|
-
const mappedCapsuleSourceLineRef = mappedCapsule.cst.capsuleSourceLineRef
|
|
1348
|
-
const solidjsComponent = extractSolidJsComponent(mappedCapsule, spineContractUri)
|
|
1349
|
-
if (solidjsComponent) {
|
|
1350
|
-
// Collect all capsule URIs from CST (mappings and property contracts)
|
|
1351
|
-
const allMappedCapsuleUris = collectAllCapsuleUris(mappedCapsule, spineContractUri)
|
|
1352
|
-
|
|
1353
|
-
// Also collect from ambient references
|
|
1354
|
-
for (const [name, ref] of Object.entries(mappedAmbientRefs)) {
|
|
1355
|
-
const refTyped = ref as any
|
|
1356
|
-
if (refTyped.type === 'capsule') {
|
|
1357
|
-
const snapshot = await buildCapsuleSnapshotForReference(refTyped, capsules, spineContractUri)
|
|
1358
|
-
const contractData = snapshot.spineContracts?.[spineContractUri]
|
|
1359
|
-
const structKey = Object.keys(contractData || {}).find(k => k.includes('/structs/Capsule'))
|
|
1360
|
-
const capsuleName = structKey ? contractData[structKey]?.capsuleName : undefined
|
|
1361
|
-
if (capsuleName) {
|
|
1362
|
-
allMappedCapsuleUris.add(capsuleName)
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
// Build capsule dependencies array
|
|
1368
|
-
const mappedCapsuleDeps: Array<{ uri: string, importName: string, importPath: string }> = []
|
|
1369
|
-
for (const uri of allMappedCapsuleUris) {
|
|
1370
|
-
const importName = `_capsule_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`
|
|
1371
|
-
// Strip leading @ to match caps filesystem paths
|
|
1372
|
-
const importPath = uri.startsWith('@') ? uri.substring(1) : uri
|
|
1373
|
-
mappedCapsuleDeps.push({ uri, importName, importPath })
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// Generate static imports for all capsule dependencies
|
|
1377
|
-
// Compute relative path from projected file to caps directory
|
|
1378
|
-
let importPrefix: string
|
|
1379
|
-
if (capsuleModuleProjectionPackage) {
|
|
1380
|
-
importPrefix = capsuleModuleProjectionPackage
|
|
1381
|
-
} else {
|
|
1382
|
-
const projectedFileDir = dirname(mapped.projectionPath)
|
|
1383
|
-
const capsDir = '.~o/encapsulate.dev/caps'
|
|
1384
|
-
const relativePathToCaps = relative(projectedFileDir, capsDir)
|
|
1385
|
-
importPrefix = relativePathToCaps.startsWith('.') ? relativePathToCaps : './' + relativePathToCaps
|
|
1386
|
-
}
|
|
1387
|
-
const mappedCapsuleImports = mappedCapsuleDeps.map(dep =>
|
|
1388
|
-
`import * as ${dep.importName} from '${importPrefix}/${dep.importPath}'`
|
|
1389
|
-
).join('\n')
|
|
1390
|
-
|
|
1391
|
-
// Generate capsules map
|
|
1392
|
-
const mappedCapsulesMapEntries = mappedCapsuleDeps.map(dep =>
|
|
1393
|
-
` '${dep.uri}': ${dep.importName}`
|
|
1394
|
-
).join(',\n')
|
|
1395
|
-
|
|
1396
|
-
mappedDefaultExport = `
|
|
1397
|
-
${mappedCapsuleImports}
|
|
1398
|
-
|
|
1399
|
-
// Set up runtime for browser execution
|
|
1400
|
-
const sourceSpine: { encapsulate?: any } = {}
|
|
1401
|
-
|
|
1402
|
-
// Map of statically imported capsules
|
|
1403
|
-
const capsulesMap: Record<string, any> = {
|
|
1404
|
-
${mappedCapsulesMapEntries}
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// Helper to import and instantiate a capsule from the capsules map
|
|
1408
|
-
const importCapsule = async (uri: string) => {
|
|
1409
|
-
const capsuleModule = capsulesMap[uri]
|
|
1410
|
-
if (!capsuleModule) {
|
|
1411
|
-
throw new Error(\`Capsule not found in static imports: \${uri}\`)
|
|
1412
|
-
}
|
|
1413
|
-
const capsule = await capsuleModule.capsule({
|
|
1414
|
-
encapsulate: sourceSpine.encapsulate,
|
|
1415
|
-
loadCapsule
|
|
1416
|
-
})
|
|
1417
|
-
return capsule
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
const loadCapsule = async ({ capsuleSourceLineRef, capsuleName }: any) => {
|
|
1421
|
-
// Return the capsule function from this projected file
|
|
1422
|
-
if (capsuleSourceLineRef === '${mappedCapsuleSourceLineRef}') {
|
|
1423
|
-
return capsule
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Use capsuleName directly if provided
|
|
1427
|
-
if (capsuleName) {
|
|
1428
|
-
return await importCapsule(capsuleName)
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
throw new Error(\`Cannot load capsule: \${capsuleSourceLineRef}\`)
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
const spineContractOpts = {
|
|
1435
|
-
spineFilesystemRoot: '.',
|
|
1436
|
-
resolve: async (uri: string) => uri,
|
|
1437
|
-
importCapsule
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
const runtimeSpineContracts = {
|
|
1441
|
-
['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(spineContractOpts)
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
const snapshot = {
|
|
1445
|
-
capsules: {
|
|
1446
|
-
['${mappedCapsuleSourceLineRef}']: {
|
|
1447
|
-
spineContracts: {}
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// Export wrapper function that initializes runtime and returns component
|
|
1453
|
-
export default function({ onMembraneEvent }: { onMembraneEvent?: (event: any) => void } = {}) {
|
|
1454
|
-
const [component, setComponent] = createSignal(null)
|
|
1455
|
-
|
|
1456
|
-
onMount(async () => {
|
|
1457
|
-
// Add onMembraneEvent to spine contract opts - use provided or default logger
|
|
1458
|
-
const defaultMembraneLogger = (event: any) => {
|
|
1459
|
-
console.log('[Membrane Event]', event.type, event)
|
|
1460
|
-
}
|
|
1461
|
-
const opts = {
|
|
1462
|
-
...spineContractOpts,
|
|
1463
|
-
onMembraneEvent: onMembraneEvent || defaultMembraneLogger
|
|
1464
|
-
}
|
|
1465
|
-
const contracts = {
|
|
1466
|
-
['#' + CapsuleSpineContract['#']]: CapsuleSpineContract(opts)
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
const { encapsulate, capsules } = await Spine({
|
|
1470
|
-
spineFilesystemRoot: '.',
|
|
1471
|
-
spineContracts: contracts
|
|
1472
|
-
})
|
|
1473
|
-
|
|
1474
|
-
sourceSpine.encapsulate = encapsulate
|
|
1475
|
-
|
|
1476
|
-
const capsuleInstance = await capsule({ encapsulate, loadCapsule })
|
|
1477
|
-
|
|
1478
|
-
const { run } = await SpineRuntime({
|
|
1479
|
-
spineFilesystemRoot: '.',
|
|
1480
|
-
spineContracts: contracts,
|
|
1481
|
-
snapshot,
|
|
1482
|
-
loadCapsule
|
|
1483
|
-
})
|
|
1484
|
-
|
|
1485
|
-
const Component = await run({}, async ({ apis }) => {
|
|
1486
|
-
const capsuleApi = apis['${mappedCapsuleSourceLineRef}']
|
|
1487
|
-
const solidjsKey = Object.keys(capsuleApi).find(k => k === 'solidjs.com/standalone')
|
|
1488
|
-
if (!solidjsKey) throw new Error('solidjs.com/standalone property not found')
|
|
1489
|
-
return capsuleApi[solidjsKey]()
|
|
1490
|
-
})
|
|
1491
|
-
|
|
1492
|
-
setComponent(() => Component)
|
|
1493
|
-
})
|
|
1494
|
-
|
|
1495
|
-
return () => {
|
|
1496
|
-
const Component = component()
|
|
1497
|
-
return Show({ when: Component, children: (Component) => Component() })
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
`
|
|
1501
|
-
}
|
|
1502
|
-
} else if (mappedHasStandalone) {
|
|
1078
|
+
if (mappedHasStandalone) {
|
|
1503
1079
|
// Generate a wrapper function that directly invokes the standalone function
|
|
1504
1080
|
const mappedCapsuleSourceLineRef = mappedCapsule.cst.capsuleSourceLineRef
|
|
1505
1081
|
|
|
@@ -1570,6 +1146,7 @@ ${mappedCapsulesMapEntries}
|
|
|
1570
1146
|
}
|
|
1571
1147
|
const capsule = await capsuleModule.capsule({
|
|
1572
1148
|
encapsulate: sourceSpine.encapsulate,
|
|
1149
|
+
CapsulePropertyTypes,
|
|
1573
1150
|
loadCapsule
|
|
1574
1151
|
})
|
|
1575
1152
|
return capsule
|
|
@@ -1726,10 +1303,38 @@ ${mappedDefaultExport}
|
|
|
1726
1303
|
.filter(Boolean)
|
|
1727
1304
|
.join('\n ')
|
|
1728
1305
|
|
|
1729
|
-
//
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1306
|
+
// Filter out makeImportStack from ambient references (same as main capsule path)
|
|
1307
|
+
let filteredCapsuleAmbientRefs = capsuleAmbientRefs
|
|
1308
|
+
if (filteredCapsuleAmbientRefs['makeImportStack']) {
|
|
1309
|
+
filteredCapsuleAmbientRefs = { ...filteredCapsuleAmbientRefs }
|
|
1310
|
+
delete filteredCapsuleAmbientRefs['makeImportStack']
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Generate imports from filtered ambient references
|
|
1314
|
+
const capsuleImportStatements = Object.entries(filteredCapsuleAmbientRefs)
|
|
1315
|
+
.map(([name, ref]: [string, any]) => {
|
|
1316
|
+
if (ref.type === 'import') {
|
|
1317
|
+
if (ref.moduleUri.endsWith('.css')) {
|
|
1318
|
+
return `import '${ref.moduleUri}'`
|
|
1319
|
+
}
|
|
1320
|
+
return `import ${ref.importSpecifier} from '${ref.moduleUri}'`
|
|
1321
|
+
}
|
|
1322
|
+
if (ref.type === 'assigned') {
|
|
1323
|
+
if (ref.moduleUri.includes('/spine-factories/')) {
|
|
1324
|
+
return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (ref.type === 'invocation-argument') {
|
|
1328
|
+
if (ref.isEncapsulateExport) {
|
|
1329
|
+
return `import { ${name} } from '@stream44.studio/encapsulate/encapsulate'`
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return ''
|
|
1333
|
+
})
|
|
1334
|
+
.filter(Boolean)
|
|
1335
|
+
.join('\n')
|
|
1336
|
+
|
|
1337
|
+
const imports = capsuleImportStatements ? capsuleImportStatements + '\n' : ''
|
|
1733
1338
|
|
|
1734
1339
|
// Get the capsule name for the assignment
|
|
1735
1340
|
const capsuleName = registryCapsule.cst.source.capsuleName || ''
|
package/src/encapsulate.ts
CHANGED
|
@@ -7,7 +7,16 @@ type TSpineOptions = {
|
|
|
7
7
|
spineFilesystemRoot?: string,
|
|
8
8
|
spineContracts: Record<string, any>,
|
|
9
9
|
staticAnalyzer?: any,
|
|
10
|
-
timing?: { record: (step: string) => void, chalk?: any }
|
|
10
|
+
timing?: { record: (step: string) => void, chalk?: any },
|
|
11
|
+
projectionContext?: {
|
|
12
|
+
capsuleModuleProjectionPackage?: string,
|
|
13
|
+
capsuleModuleProjectionRoot?: string,
|
|
14
|
+
projectionStore?: {
|
|
15
|
+
writeFile: (filepath: string, content: string) => Promise<void>,
|
|
16
|
+
getStats?: (filepath: string) => Promise<{ mtime: Date } | null>
|
|
17
|
+
} | null,
|
|
18
|
+
capsules?: Record<string, any>
|
|
19
|
+
}
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
type TSpineRunOptions = {
|
|
@@ -115,6 +124,7 @@ export const CapsulePropertyTypes = {
|
|
|
115
124
|
StructDispose: 'StructDispose' as const,
|
|
116
125
|
Init: 'Init' as const,
|
|
117
126
|
Dispose: 'Dispose' as const,
|
|
127
|
+
OnFreeze: 'OnFreeze' as const,
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
// ##################################################
|
|
@@ -430,7 +440,13 @@ export async function Spine(options: TSpineOptions): Promise<TSpine> {
|
|
|
430
440
|
|
|
431
441
|
options.timing?.record(`Spine: Freezing ${Object.keys(capsules).length} capsules`)
|
|
432
442
|
|
|
433
|
-
|
|
443
|
+
const processedCapsules = new Set<any>()
|
|
444
|
+
const freezeVisited = new Set<any>()
|
|
445
|
+
for (const [capsuleSourceLineRef, capsule] of Object.entries(capsules)) {
|
|
446
|
+
|
|
447
|
+
// Skip capsuleName aliases — only process each capsule once via its capsuleSourceLineRef
|
|
448
|
+
if (processedCapsules.has(capsule)) continue
|
|
449
|
+
processedCapsules.add(capsule)
|
|
434
450
|
|
|
435
451
|
if (!capsule.cst.source.capsuleName) throw new Error(`'capsuleName' must be set for encapsulate options to enable freezing.`)
|
|
436
452
|
|
|
@@ -438,10 +454,95 @@ export async function Spine(options: TSpineOptions): Promise<TSpine> {
|
|
|
438
454
|
cst: capsule.cst,
|
|
439
455
|
spineContracts: {}
|
|
440
456
|
}
|
|
457
|
+
// Also register under capsuleName so SpineRuntime can resolve by name
|
|
458
|
+
if (capsule.cst.source.capsuleName && capsule.cst.source.capsuleName !== capsuleSourceLineRef) {
|
|
459
|
+
snapshot.capsules[capsule.cst.source.capsuleName] = snapshot.capsules[capsuleSourceLineRef]
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const capsuleInstance = await capsule.makeInstance()
|
|
463
|
+
|
|
464
|
+
// Run OnFreeze functions so capsules can perform side effects
|
|
465
|
+
// (e.g. file projection) at build/freeze time
|
|
466
|
+
async function runOnFreeze(instance: any, parentCapsuleCst?: any, parentCapsuleSourceLineRef?: string, projectionPath?: string, projectionSpineContractUri?: string) {
|
|
467
|
+
if (!instance) return
|
|
468
|
+
// Use capsuleSourceLineRef for deduplication — this is stable across different
|
|
469
|
+
// instance objects of the same capsule (top-level vs mapped child).
|
|
470
|
+
// Include projectionPath in the key so the same capsule can project to
|
|
471
|
+
// multiple output paths (e.g. standalone delegate used by different parents).
|
|
472
|
+
// Fall back to instance identity for internal capsules without a lineRef.
|
|
473
|
+
// Use capsuleName as the base key when available (always a unique string),
|
|
474
|
+
// otherwise fall back to capsuleSourceLineRef or instance identity.
|
|
475
|
+
// This avoids collisions from object refs that all stringify to '[object Object]'.
|
|
476
|
+
const baseKey = instance.capsuleName || instance.capsuleSourceLineRef || instance
|
|
477
|
+
const freezeKey = projectionPath ? `${baseKey}::${projectionPath}` : baseKey
|
|
478
|
+
if (freezeVisited.has(freezeKey)) return
|
|
479
|
+
freezeVisited.add(freezeKey)
|
|
480
|
+
|
|
481
|
+
// Inject projection context onto CapsuleProjectionContext instances
|
|
482
|
+
// Set values on both the api (encapsulatedApi) and spine contract self
|
|
483
|
+
// so they are visible through all proxy chains.
|
|
484
|
+
// Scan recursively into mapped children since CapsuleProjectionContext
|
|
485
|
+
// may be nested inside a property contract delegate (e.g. a projector capsule).
|
|
486
|
+
const projectionCtx = options.projectionContext
|
|
487
|
+
if (projectionCtx && instance.mappedCapsuleInstances?.length) {
|
|
488
|
+
const injectCtx = (children: any[]) => {
|
|
489
|
+
for (const mappedChild of children) {
|
|
490
|
+
const childApi = mappedChild.api
|
|
491
|
+
if (childApi && mappedChild.capsuleName === '@stream44.studio/encapsulate/structs/CapsuleProjectionContext') {
|
|
492
|
+
const ctx = {
|
|
493
|
+
parentCapsuleCst: parentCapsuleCst,
|
|
494
|
+
parentCapsuleSourceLineRef: parentCapsuleSourceLineRef,
|
|
495
|
+
capsuleModuleProjectionPackage: projectionCtx.capsuleModuleProjectionPackage,
|
|
496
|
+
projectionStore: projectionCtx.projectionStore,
|
|
497
|
+
capsuleSnapshots: projectionCtx.capsules,
|
|
498
|
+
projectionPath: projectionPath,
|
|
499
|
+
spineContractUri: projectionSpineContractUri,
|
|
500
|
+
}
|
|
501
|
+
Object.assign(childApi, ctx)
|
|
502
|
+
// Also update spine contract self for selfProxy access
|
|
503
|
+
for (const childSci of Object.values(mappedChild.spineContractCapsuleInstances || {})) {
|
|
504
|
+
const childSelf = (childSci as any).self
|
|
505
|
+
if (childSelf) Object.assign(childSelf, ctx)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Recurse into delegate's mapped children
|
|
509
|
+
if (mappedChild.mappedCapsuleInstances?.length) {
|
|
510
|
+
injectCtx(mappedChild.mappedCapsuleInstances)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
injectCtx(instance.mappedCapsuleInstances)
|
|
515
|
+
}
|
|
441
516
|
|
|
442
|
-
|
|
517
|
+
if (instance.onFreezeFunctions?.length) {
|
|
518
|
+
for (const fn of instance.onFreezeFunctions) {
|
|
519
|
+
await fn()
|
|
520
|
+
}
|
|
521
|
+
}
|
|
443
522
|
|
|
444
|
-
|
|
523
|
+
if (instance.extendedCapsuleInstance) {
|
|
524
|
+
await runOnFreeze(instance.extendedCapsuleInstance, parentCapsuleCst, parentCapsuleSourceLineRef, projectionPath, projectionSpineContractUri)
|
|
525
|
+
}
|
|
526
|
+
if (instance.mappedCapsuleInstances?.length) {
|
|
527
|
+
for (const mappedInstance of instance.mappedCapsuleInstances) {
|
|
528
|
+
// Determine projection path from the property name (alias) used for this mapping.
|
|
529
|
+
// If the mapped child doesn't define its own path (no / prefix), inherit the parent's projectionPath
|
|
530
|
+
// so it flows down to property contract delegates.
|
|
531
|
+
const mappedProjectionPath = mappedInstance.mappedPropertyName?.startsWith('/') ? mappedInstance.mappedPropertyName : projectionPath
|
|
532
|
+
// For regular mapped capsules (non-struct delegates), use their own CST as
|
|
533
|
+
// parentCapsuleCst so nested property contract delegates see the correct parent.
|
|
534
|
+
// Property contract delegates (flagged by Static.v0) pass through the current
|
|
535
|
+
// parentCapsuleCst so their OnFreeze sees the declaring capsule's CST.
|
|
536
|
+
const mappedCapsule = (!mappedInstance.isPropertyContractDelegate && mappedInstance.capsuleName) ? capsules[mappedInstance.capsuleName] : undefined
|
|
537
|
+
const childParentCst = mappedCapsule?.cst || parentCapsuleCst
|
|
538
|
+
const instanceSourceLineRef = instance.capsuleSourceLineRef || parentCapsuleSourceLineRef
|
|
539
|
+
await runOnFreeze(mappedInstance, childParentCst, instanceSourceLineRef, mappedProjectionPath, Object.keys(capsuleInstance.spineContractCapsuleInstances)?.[0])
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
await runOnFreeze(capsuleInstance, capsule.cst, capsule.capsuleSourceLineRef)
|
|
544
|
+
|
|
545
|
+
await Promise.all(Object.entries(capsuleInstance.spineContractCapsuleInstances).map(async ([spineContractUri, spineContractCapsuleInstance]) => {
|
|
445
546
|
|
|
446
547
|
snapshot.capsules[capsuleSourceLineRef] = merge(
|
|
447
548
|
snapshot.capsules[capsuleSourceLineRef],
|
|
@@ -451,7 +552,7 @@ export async function Spine(options: TSpineOptions): Promise<TSpine> {
|
|
|
451
552
|
})
|
|
452
553
|
)
|
|
453
554
|
}))
|
|
454
|
-
}
|
|
555
|
+
}
|
|
455
556
|
|
|
456
557
|
options.timing?.record('Spine: Freeze complete')
|
|
457
558
|
|
|
@@ -621,7 +722,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
621
722
|
// Check if 'as' is defined to use as the property name alias
|
|
622
723
|
const propDefTyped = propDef as Record<string, any>
|
|
623
724
|
const aliasName = propDefTyped.as
|
|
624
|
-
|
|
725
|
+
let delegateOptions = propDefTyped.options
|
|
625
726
|
const contractKey = aliasName || ('#' + propContractUri.substring(1))
|
|
626
727
|
|
|
627
728
|
if (!propertyContractDefinitions[spineContractUri]['#']) {
|
|
@@ -835,8 +936,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
835
936
|
moduleFilepath: absoluteModuleFilepath
|
|
836
937
|
},
|
|
837
938
|
parentCapsuleSourceUriLineRefInstanceId: parentCapsuleSourceUriLineRefInstanceId
|
|
838
|
-
? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + (cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef))
|
|
839
|
-
: sha256(cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef),
|
|
939
|
+
? await sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + (cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef))
|
|
940
|
+
: await sha256(cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef),
|
|
840
941
|
sit
|
|
841
942
|
})
|
|
842
943
|
|
|
@@ -873,8 +974,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
873
974
|
// child: sha256(parentCapsuleSourceUriLineRefInstanceId + ":" + capsuleSourceUriLineRef)
|
|
874
975
|
const capsuleSourceUriLineRef = cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef
|
|
875
976
|
const capsuleSourceUriLineRefInstanceId = parentCapsuleSourceUriLineRefInstanceId
|
|
876
|
-
? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + capsuleSourceUriLineRef)
|
|
877
|
-
: sha256(capsuleSourceUriLineRef)
|
|
977
|
+
? await sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + capsuleSourceUriLineRef)
|
|
978
|
+
: await sha256(capsuleSourceUriLineRef)
|
|
878
979
|
|
|
879
980
|
// Register this instance in the sit structure if provided
|
|
880
981
|
if (sit) {
|
|
@@ -893,6 +994,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
893
994
|
structDisposeFunctions: [] as Array<() => any>,
|
|
894
995
|
initFunctions: [] as Array<() => any>,
|
|
895
996
|
disposeFunctions: [] as Array<() => any>,
|
|
997
|
+
onFreezeFunctions: [] as Array<() => any>,
|
|
896
998
|
mappedCapsuleInstances: [] as Array<any>,
|
|
897
999
|
rootCapsule: resolvedRootCapsule,
|
|
898
1000
|
capsuleSourceUriLineRefInstanceId,
|
|
@@ -987,6 +1089,9 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
987
1089
|
if (sci.disposeFunctions?.length) {
|
|
988
1090
|
capsuleInstance.disposeFunctions.push(...sci.disposeFunctions)
|
|
989
1091
|
}
|
|
1092
|
+
if (sci.onFreezeFunctions?.length) {
|
|
1093
|
+
capsuleInstance.onFreezeFunctions.push(...sci.onFreezeFunctions)
|
|
1094
|
+
}
|
|
990
1095
|
if (sci.mappedCapsuleInstances?.length) {
|
|
991
1096
|
capsuleInstance.mappedCapsuleInstances.push(...sci.mappedCapsuleInstances)
|
|
992
1097
|
}
|
|
@@ -1124,15 +1229,15 @@ function relative(from: string, to: string): string {
|
|
|
1124
1229
|
return result || '.'
|
|
1125
1230
|
}
|
|
1126
1231
|
|
|
1127
|
-
function sha256(input: string): string {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1232
|
+
async function sha256(input: string): Promise<string> {
|
|
1233
|
+
const data = new TextEncoder().encode(input)
|
|
1234
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
1235
|
+
const hashArray = new Uint8Array(hashBuffer)
|
|
1236
|
+
let hex = ''
|
|
1237
|
+
for (let i = 0; i < hashArray.length; i++) {
|
|
1238
|
+
hex += hashArray[i].toString(16).padStart(2, '0')
|
|
1133
1239
|
}
|
|
1134
|
-
|
|
1135
|
-
return createHash('sha256').update(input).digest('hex')
|
|
1240
|
+
return hex
|
|
1136
1241
|
}
|
|
1137
1242
|
|
|
1138
1243
|
function isObject(item: any): boolean {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { CapsulePropertyTypes } from "../../encapsulate"
|
|
2
2
|
import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static.v0"
|
|
3
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
4
|
-
import { dirname, relative, join } from "node:path"
|
|
5
3
|
|
|
6
4
|
type CallerContext = {
|
|
7
5
|
capsuleSourceLineRef: string
|
|
@@ -60,6 +58,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
60
58
|
private setCurrentCallerContext: (ctx: CallerContext | undefined) => void
|
|
61
59
|
private onMembraneEvent?: (event: any) => void
|
|
62
60
|
private enableCallerStackInference: boolean
|
|
61
|
+
private npmUriForFilepathSync?: (filepath: string) => string | null
|
|
63
62
|
private encapsulateOptions: any
|
|
64
63
|
private capsuleSourceNameRef?: string
|
|
65
64
|
private capsuleSourceNameRefHash?: string
|
|
@@ -78,6 +77,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
78
77
|
freezeCapsule,
|
|
79
78
|
onMembraneEvent,
|
|
80
79
|
enableCallerStackInference,
|
|
80
|
+
npmUriForFilepathSync,
|
|
81
81
|
encapsulateOptions,
|
|
82
82
|
getEventIndex,
|
|
83
83
|
incrementEventIndex,
|
|
@@ -99,6 +99,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
99
99
|
freezeCapsule?: (capsule: any) => Promise<any>
|
|
100
100
|
onMembraneEvent?: (event: any) => void
|
|
101
101
|
enableCallerStackInference: boolean
|
|
102
|
+
npmUriForFilepathSync?: (filepath: string) => string | null
|
|
102
103
|
encapsulateOptions: any
|
|
103
104
|
getEventIndex: () => number
|
|
104
105
|
incrementEventIndex: () => number
|
|
@@ -116,6 +117,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
116
117
|
this.setCurrentCallerContext = setCurrentCallerContext
|
|
117
118
|
this.onMembraneEvent = onMembraneEvent
|
|
118
119
|
this.enableCallerStackInference = enableCallerStackInference
|
|
120
|
+
this.npmUriForFilepathSync = npmUriForFilepathSync
|
|
119
121
|
this.encapsulateOptions = encapsulateOptions
|
|
120
122
|
this.capsuleSourceNameRef = capsule?.cst?.capsuleSourceNameRef
|
|
121
123
|
this.capsuleSourceNameRefHash = capsule?.cst?.capsuleSourceNameRefHash
|
|
@@ -184,7 +186,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
184
186
|
if (this.enableCallerStackInference) {
|
|
185
187
|
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
186
188
|
if (stackStr) {
|
|
187
|
-
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
189
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot, this.npmUriForFilepathSync)
|
|
188
190
|
if (stackFrames.length > 0) {
|
|
189
191
|
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
190
192
|
callerCtx.fileUri = callerInfo.fileUri
|
|
@@ -311,7 +313,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
311
313
|
if (this.enableCallerStackInference) {
|
|
312
314
|
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
313
315
|
if (stackStr) {
|
|
314
|
-
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
316
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot, this.npmUriForFilepathSync)
|
|
315
317
|
if (stackFrames.length > 0) {
|
|
316
318
|
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
317
319
|
callerCtx.fileUri = callerInfo.fileUri
|
|
@@ -374,7 +376,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
374
376
|
if (this.enableCallerStackInference) {
|
|
375
377
|
const stackStr = new Error('[PROPERTY_CONTRACT_DELEGATE]').stack
|
|
376
378
|
if (stackStr) {
|
|
377
|
-
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
379
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot, this.npmUriForFilepathSync)
|
|
378
380
|
if (stackFrames.length > 0) {
|
|
379
381
|
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
380
382
|
callerCtx.fileUri = callerInfo.fileUri
|
|
@@ -892,7 +894,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
892
894
|
} else if (this.enableCallerStackInference) {
|
|
893
895
|
const stackStr = new Error('[MEMBRANE_EVENT]').stack
|
|
894
896
|
if (stackStr) {
|
|
895
|
-
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
897
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot, this.npmUriForFilepathSync)
|
|
896
898
|
if (stackFrames.length > 0) {
|
|
897
899
|
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
898
900
|
event.caller = {
|
|
@@ -911,7 +913,8 @@ export function CapsuleSpineContract({
|
|
|
911
913
|
enableCallerStackInference = false,
|
|
912
914
|
spineFilesystemRoot,
|
|
913
915
|
resolve,
|
|
914
|
-
importCapsule
|
|
916
|
+
importCapsule,
|
|
917
|
+
npmUriForFilepath
|
|
915
918
|
}: {
|
|
916
919
|
onMembraneEvent?: (event: any) => void
|
|
917
920
|
freezeCapsule?: (capsule: any) => Promise<any>
|
|
@@ -919,12 +922,28 @@ export function CapsuleSpineContract({
|
|
|
919
922
|
spineFilesystemRoot?: string
|
|
920
923
|
resolve?: (uri: string, parentFilepath: string) => Promise<string>
|
|
921
924
|
importCapsule?: (filepath: string) => Promise<any>
|
|
925
|
+
npmUriForFilepath?: (filepath: string) => Promise<string | null>
|
|
922
926
|
} = {}) {
|
|
923
927
|
|
|
924
928
|
let eventIndex = 0
|
|
925
929
|
let currentCallerContext: CallerContext | undefined = undefined
|
|
926
930
|
const instanceRegistry: CapsuleInstanceRegistry = new Map()
|
|
927
931
|
|
|
932
|
+
// Sync cache for npmUriForFilepath — async calls populate the cache,
|
|
933
|
+
// sync reads return cached values (raw filepath fallback on cache miss).
|
|
934
|
+
const npmUriCache = new Map<string, string | null>()
|
|
935
|
+
const npmUriForFilepathSync = npmUriForFilepath
|
|
936
|
+
? (filepath: string): string | null => {
|
|
937
|
+
if (npmUriCache.has(filepath)) return npmUriCache.get(filepath)!
|
|
938
|
+
// Fire async resolution to populate cache for next access
|
|
939
|
+
npmUriForFilepath(filepath).then(
|
|
940
|
+
uri => npmUriCache.set(filepath, uri),
|
|
941
|
+
() => npmUriCache.set(filepath, null)
|
|
942
|
+
)
|
|
943
|
+
return null
|
|
944
|
+
}
|
|
945
|
+
: undefined
|
|
946
|
+
|
|
928
947
|
// Re-entrancy guard: suppress event emission while inside an onMembraneEvent callback.
|
|
929
948
|
// This prevents consumers (e.g. JSON.stringify on event.value) from triggering proxy getters
|
|
930
949
|
// that would cause spurious recursive membrane events with wrong caller context and ordering.
|
|
@@ -955,6 +974,7 @@ export function CapsuleSpineContract({
|
|
|
955
974
|
importCapsule,
|
|
956
975
|
onMembraneEvent: guardedOnMembraneEvent,
|
|
957
976
|
enableCallerStackInference,
|
|
977
|
+
npmUriForFilepathSync,
|
|
958
978
|
encapsulateOptions,
|
|
959
979
|
getEventIndex: () => eventIndex,
|
|
960
980
|
incrementEventIndex: () => eventIndex++,
|
|
@@ -975,67 +995,7 @@ export function CapsuleSpineContract({
|
|
|
975
995
|
CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'
|
|
976
996
|
|
|
977
997
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
// Cache for synchronous npm URI lookups (directory -> package name or null)
|
|
981
|
-
const npmUriCache = new Map<string, string | null>()
|
|
982
|
-
|
|
983
|
-
function constructNpmUriSync(absoluteFilepath: string): string | null {
|
|
984
|
-
// Only process absolute paths — skip V8 internal markers like "native", "node:*", etc.
|
|
985
|
-
if (!absoluteFilepath.startsWith('/')) {
|
|
986
|
-
return null
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Check for /node_modules/ in the path — use the last occurrence to handle nested node_modules
|
|
990
|
-
const nodeModulesMarker = '/node_modules/'
|
|
991
|
-
const lastIdx = absoluteFilepath.lastIndexOf(nodeModulesMarker)
|
|
992
|
-
if (lastIdx !== -1) {
|
|
993
|
-
return absoluteFilepath.substring(lastIdx + nodeModulesMarker.length)
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
let currentDir = dirname(absoluteFilepath)
|
|
997
|
-
const maxDepth = 20
|
|
998
|
-
|
|
999
|
-
for (let i = 0; i < maxDepth; i++) {
|
|
1000
|
-
if (npmUriCache.has(currentDir)) {
|
|
1001
|
-
const cachedName = npmUriCache.get(currentDir)
|
|
1002
|
-
if (cachedName) {
|
|
1003
|
-
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
1004
|
-
return `${cachedName}/${relativeFromPackage}`
|
|
1005
|
-
}
|
|
1006
|
-
// null means no package.json with name found at this level, continue up
|
|
1007
|
-
const parentDir = dirname(currentDir)
|
|
1008
|
-
if (parentDir === currentDir) break
|
|
1009
|
-
currentDir = parentDir
|
|
1010
|
-
continue
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
const packageJsonPath = join(currentDir, 'package.json')
|
|
1014
|
-
try {
|
|
1015
|
-
if (existsSync(packageJsonPath)) {
|
|
1016
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
1017
|
-
const packageName = packageJson.name
|
|
1018
|
-
npmUriCache.set(currentDir, packageName || null)
|
|
1019
|
-
if (packageName) {
|
|
1020
|
-
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
1021
|
-
return `${packageName}/${relativeFromPackage}`
|
|
1022
|
-
}
|
|
1023
|
-
} else {
|
|
1024
|
-
npmUriCache.set(currentDir, null)
|
|
1025
|
-
}
|
|
1026
|
-
} catch {
|
|
1027
|
-
npmUriCache.set(currentDir, null)
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const parentDir = dirname(currentDir)
|
|
1031
|
-
if (parentDir === currentDir) break
|
|
1032
|
-
currentDir = parentDir
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
return null
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, fileUri?: string, line?: number, column?: number }> {
|
|
998
|
+
function parseCallerFromStack(stack: string, spineFilesystemRoot?: string, npmUriForFilepathSync?: (filepath: string) => string | null): Array<{ function?: string, fileUri?: string, line?: number, column?: number }> {
|
|
1039
999
|
const lines = stack.split('\n')
|
|
1040
1000
|
const result: Array<{ function?: string, fileUri?: string, line?: number, column?: number }> = []
|
|
1041
1001
|
|
|
@@ -1082,7 +1042,7 @@ function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Arra
|
|
|
1082
1042
|
|
|
1083
1043
|
// Convert absolute filepaths to npm URIs
|
|
1084
1044
|
if (rawFilepath) {
|
|
1085
|
-
const npmUri =
|
|
1045
|
+
const npmUri = npmUriForFilepathSync ? npmUriForFilepathSync(rawFilepath) : null
|
|
1086
1046
|
if (npmUri) {
|
|
1087
1047
|
// Strip file extension from URI for consistency
|
|
1088
1048
|
frame.fileUri = npmUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
@@ -23,6 +23,7 @@ export class ContractCapsuleInstanceFactory {
|
|
|
23
23
|
public structDisposeFunctions: Array<() => any> = []
|
|
24
24
|
public initFunctions: Array<() => any> = []
|
|
25
25
|
public disposeFunctions: Array<() => any> = []
|
|
26
|
+
public onFreezeFunctions: Array<() => any> = []
|
|
26
27
|
public mappedCapsuleInstances: Array<any> = []
|
|
27
28
|
protected memoizeCache: Map<string, any> = new Map()
|
|
28
29
|
protected memoizeTimeouts: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
@@ -104,6 +105,8 @@ export class ContractCapsuleInstanceFactory {
|
|
|
104
105
|
this.mapInitProperty({ property })
|
|
105
106
|
} else if (property.definition.type === CapsulePropertyTypes.Dispose) {
|
|
106
107
|
this.mapDisposeProperty({ property })
|
|
108
|
+
} else if (property.definition.type === CapsulePropertyTypes.OnFreeze) {
|
|
109
|
+
this.mapOnFreezeProperty({ property })
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -212,9 +215,12 @@ export class ContractCapsuleInstanceFactory {
|
|
|
212
215
|
|
|
213
216
|
// Check for existing instance in registry - reuse if available when no options
|
|
214
217
|
// Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
|
|
215
|
-
// Property contract delegates (structs) always get a fresh instance per parent capsule
|
|
218
|
+
// Property contract delegates (structs) always get a fresh instance per parent capsule.
|
|
219
|
+
// CapsuleProjectionContext also needs fresh instances so context injection can find it
|
|
220
|
+
// in mappedCapsuleInstances during freeze traversal.
|
|
216
221
|
const capsuleName = mappedCapsule.encapsulateOptions?.capsuleName
|
|
217
222
|
const isCapsuleStruct = property.definition.propertyContractDelegate === '#@stream44.studio/encapsulate/structs/Capsule'
|
|
223
|
+
|| property.definition.propertyContractDelegate === '#@stream44.studio/encapsulate/structs/CapsuleProjectionContext'
|
|
218
224
|
|
|
219
225
|
if (capsuleName && this.instanceRegistry && !isCapsuleStruct) {
|
|
220
226
|
if (this.instanceRegistry.has(capsuleName)) {
|
|
@@ -335,6 +341,10 @@ export class ContractCapsuleInstanceFactory {
|
|
|
335
341
|
}
|
|
336
342
|
|
|
337
343
|
apiTarget[property.name] = mappedInstance
|
|
344
|
+
mappedInstance.mappedPropertyName = property.name
|
|
345
|
+
if (property.definition.propertyContractDelegate) {
|
|
346
|
+
mappedInstance.isPropertyContractDelegate = true
|
|
347
|
+
}
|
|
338
348
|
this.mappedCapsuleInstances.push(mappedInstance)
|
|
339
349
|
// Use proxy to unwrap .api for this.self so internal references work
|
|
340
350
|
this.self[property.name] = mappedInstance.api ? new Proxy(mappedInstance.api, {
|
|
@@ -349,18 +359,17 @@ export class ContractCapsuleInstanceFactory {
|
|
|
349
359
|
}) : mappedInstance
|
|
350
360
|
|
|
351
361
|
// If this mapping has a propertyContractDelegate, also mount the mapped capsule's API
|
|
352
|
-
// to the property contract namespace for direct access
|
|
362
|
+
// to the property contract namespace for direct access.
|
|
363
|
+
// Use a proxy so that later mutations to the delegate's API (e.g. CapsuleProjectionContext
|
|
364
|
+
// injection during freeze) are visible through the parent's encapsulatedApi.
|
|
353
365
|
if (property.definition.propertyContractDelegate) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
for (const [key, value] of Object.entries(mappedInstance.api)) {
|
|
362
|
-
delegateTarget[key] = value
|
|
363
|
-
}
|
|
366
|
+
this.encapsulatedApi[property.definition.propertyContractDelegate] = new Proxy(mappedInstance.api, {
|
|
367
|
+
get: (target: any, prop: string | symbol) => target[prop],
|
|
368
|
+
set: (target: any, prop: string | symbol, value: any) => { target[prop] = value; return true },
|
|
369
|
+
ownKeys: (target: any) => Reflect.ownKeys(target),
|
|
370
|
+
getOwnPropertyDescriptor: (target: any, prop: string | symbol) => Object.getOwnPropertyDescriptor(target, prop) || { configurable: true, enumerable: true, writable: true, value: target[prop] },
|
|
371
|
+
has: (target: any, prop: string | symbol) => prop in target,
|
|
372
|
+
})
|
|
364
373
|
}
|
|
365
374
|
}
|
|
366
375
|
|
|
@@ -606,6 +615,12 @@ export class ContractCapsuleInstanceFactory {
|
|
|
606
615
|
this.disposeFunctions.push(boundFunction)
|
|
607
616
|
}
|
|
608
617
|
|
|
618
|
+
protected mapOnFreezeProperty({ property }: { property: any }) {
|
|
619
|
+
const selfProxy = this.createSelfProxy()
|
|
620
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
621
|
+
this.onFreezeFunctions.push(boundFunction)
|
|
622
|
+
}
|
|
623
|
+
|
|
609
624
|
async freeze(options: any): Promise<any> {
|
|
610
625
|
return this.freezeCapsule?.(options) || {}
|
|
611
626
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, dirname, resolve as pathResolve } from 'path'
|
|
1
|
+
import { join, dirname, relative, resolve as pathResolve } from 'path'
|
|
2
2
|
import { writeFile, mkdir, readFile, stat } from 'fs/promises'
|
|
3
3
|
import { Spine, SpineRuntime, CapsulePropertyTypes, makeImportStack, merge } from "../encapsulate"
|
|
4
4
|
import { StaticAnalyzer } from "../../src/static-analyzer.v0"
|
|
@@ -278,6 +278,65 @@ async function resolve(uri: string, fromPath: string, spineRoot?: string): Promi
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
// Build an async npmUriForFilepath resolver backed by a directory→package-name cache.
|
|
282
|
+
// Walks up the directory tree from a given filepath to find the nearest package.json
|
|
283
|
+
// and constructs an npm-style URI (e.g. "package-name/src/foo.ts").
|
|
284
|
+
function createNpmUriForFilepath(): (filepath: string) => Promise<string | null> {
|
|
285
|
+
const cache = new Map<string, string | null>()
|
|
286
|
+
|
|
287
|
+
return async (absoluteFilepath: string): Promise<string | null> => {
|
|
288
|
+
// Only process absolute paths — skip V8 internal markers like "native", "node:*", etc.
|
|
289
|
+
if (!absoluteFilepath.startsWith('/')) {
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check for /node_modules/ in the path — use the last occurrence to handle nested node_modules
|
|
294
|
+
const nodeModulesMarker = '/node_modules/'
|
|
295
|
+
const lastIdx = absoluteFilepath.lastIndexOf(nodeModulesMarker)
|
|
296
|
+
if (lastIdx !== -1) {
|
|
297
|
+
return absoluteFilepath.substring(lastIdx + nodeModulesMarker.length)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let currentDir = dirname(absoluteFilepath)
|
|
301
|
+
const maxDepth = 20
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
304
|
+
if (cache.has(currentDir)) {
|
|
305
|
+
const cachedName = cache.get(currentDir)
|
|
306
|
+
if (cachedName) {
|
|
307
|
+
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
308
|
+
return `${cachedName}/${relativeFromPackage}`
|
|
309
|
+
}
|
|
310
|
+
// null means no package.json with name found at this level, continue up
|
|
311
|
+
const parentDir = dirname(currentDir)
|
|
312
|
+
if (parentDir === currentDir) break
|
|
313
|
+
currentDir = parentDir
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const packageJsonPath = join(currentDir, 'package.json')
|
|
318
|
+
try {
|
|
319
|
+
await stat(packageJsonPath)
|
|
320
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
|
|
321
|
+
const packageName = packageJson.name
|
|
322
|
+
cache.set(currentDir, packageName || null)
|
|
323
|
+
if (packageName) {
|
|
324
|
+
const relativeFromPackage = relative(currentDir, absoluteFilepath)
|
|
325
|
+
return `${packageName}/${relativeFromPackage}`
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
cache.set(currentDir, null)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const parentDir = dirname(currentDir)
|
|
332
|
+
if (parentDir === currentDir) break
|
|
333
|
+
currentDir = parentDir
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return null
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
281
340
|
export async function CapsuleSpineFactory({
|
|
282
341
|
spineFilesystemRoot,
|
|
283
342
|
capsuleModuleProjectionRoot,
|
|
@@ -329,8 +388,10 @@ export async function CapsuleSpineFactory({
|
|
|
329
388
|
}
|
|
330
389
|
|
|
331
390
|
const sourceSpine: { encapsulate?: any } = {}
|
|
391
|
+
const npmUriForFilepath = createNpmUriForFilepath()
|
|
332
392
|
const commonSpineContractOpts = {
|
|
333
393
|
spineFilesystemRoot,
|
|
394
|
+
npmUriForFilepath,
|
|
334
395
|
resolve: async (uri: string, parentFilepath: string) => {
|
|
335
396
|
// For relative paths, join with parent directory first
|
|
336
397
|
if (/^\.\.?\//.test(uri)) {
|
|
@@ -537,7 +598,28 @@ export async function CapsuleSpineFactory({
|
|
|
537
598
|
},
|
|
538
599
|
},
|
|
539
600
|
}) : undefined,
|
|
540
|
-
spineContracts: spineContractInstances.encapsulation
|
|
601
|
+
spineContracts: spineContractInstances.encapsulation,
|
|
602
|
+
projectionContext: capsuleModuleProjectionRoot ? {
|
|
603
|
+
capsuleModuleProjectionPackage,
|
|
604
|
+
capsuleModuleProjectionRoot,
|
|
605
|
+
projectionStore: {
|
|
606
|
+
writeFile: async (filepath: string, content: string) => {
|
|
607
|
+
filepath = join(capsuleModuleProjectionRoot, filepath)
|
|
608
|
+
await mkdir(dirname(filepath), { recursive: true })
|
|
609
|
+
await writeFile(filepath, content, 'utf-8')
|
|
610
|
+
},
|
|
611
|
+
getStats: async (filepath: string) => {
|
|
612
|
+
filepath = join(capsuleModuleProjectionRoot, filepath)
|
|
613
|
+
try {
|
|
614
|
+
const stats = await stat(filepath)
|
|
615
|
+
return { mtime: stats.mtime }
|
|
616
|
+
} catch (error) {
|
|
617
|
+
return null
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
get capsules() { return capsules }
|
|
622
|
+
} : undefined
|
|
541
623
|
})
|
|
542
624
|
sourceSpine.encapsulate = encapsulate
|
|
543
625
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
export async function capsule({
|
|
3
|
+
encapsulate,
|
|
4
|
+
CapsulePropertyTypes,
|
|
5
|
+
makeImportStack
|
|
6
|
+
}: any) {
|
|
7
|
+
return encapsulate({
|
|
8
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
9
|
+
'#': {
|
|
10
|
+
// The parent capsule's full CST (source, spineContracts, ambient references, etc.)
|
|
11
|
+
parentCapsuleCst: {
|
|
12
|
+
type: CapsulePropertyTypes.Literal,
|
|
13
|
+
value: undefined
|
|
14
|
+
},
|
|
15
|
+
// The parent capsule's capsuleSourceLineRef
|
|
16
|
+
parentCapsuleSourceLineRef: {
|
|
17
|
+
type: CapsulePropertyTypes.Literal,
|
|
18
|
+
value: undefined
|
|
19
|
+
},
|
|
20
|
+
// Package prefix for projected capsule imports (e.g. '~caps')
|
|
21
|
+
capsuleModuleProjectionPackage: {
|
|
22
|
+
type: CapsulePropertyTypes.Literal,
|
|
23
|
+
value: undefined
|
|
24
|
+
},
|
|
25
|
+
// Store for writing projected files
|
|
26
|
+
projectionStore: {
|
|
27
|
+
type: CapsulePropertyTypes.Literal,
|
|
28
|
+
value: undefined
|
|
29
|
+
},
|
|
30
|
+
// All capsule snapshots for dependency resolution
|
|
31
|
+
capsuleSnapshots: {
|
|
32
|
+
type: CapsulePropertyTypes.Literal,
|
|
33
|
+
value: undefined
|
|
34
|
+
},
|
|
35
|
+
// The property contract delegate alias (e.g. '/apps/web/src/components/Counter1.tsx')
|
|
36
|
+
projectionPath: {
|
|
37
|
+
type: CapsulePropertyTypes.Literal,
|
|
38
|
+
value: undefined
|
|
39
|
+
},
|
|
40
|
+
// Spine contract URI
|
|
41
|
+
spineContractUri: {
|
|
42
|
+
type: CapsulePropertyTypes.Literal,
|
|
43
|
+
value: undefined
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, {
|
|
48
|
+
importMeta: import.meta,
|
|
49
|
+
importStack: makeImportStack(),
|
|
50
|
+
capsuleName: capsule['#'],
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
capsule['#'] = '@stream44.studio/encapsulate/structs/CapsuleProjectionContext'
|