@stream44.studio/encapsulate 0.4.0-rc.11

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.
@@ -0,0 +1,541 @@
1
+ import { join, dirname, resolve as pathResolve } from 'path'
2
+ import { writeFile, mkdir, readFile, stat } from 'fs/promises'
3
+ import { Spine, SpineRuntime, CapsulePropertyTypes, makeImportStack, merge } from "../encapsulate"
4
+ import { StaticAnalyzer } from "../../src/static-analyzer.v0"
5
+ import { CapsuleModuleProjector } from "../../src/capsule-projectors/CapsuleModuleProjector.v0"
6
+
7
+
8
+ export { merge }
9
+
10
+ // Pure filesystem resolve — never calls Bun.resolve or createRequire to avoid
11
+ // Bun's internal module resolver which can OOM on large capsule trees.
12
+ //
13
+ // Resolution strategy:
14
+ // 1. Scoped packages: @scope/package/path → spineRoot/scope/packages/package/path.ts
15
+ // (matches tsconfig.paths.json pattern used across the workspace)
16
+ // 2. Scoped package root: @scope/package → resolve via package.json exports
17
+ // 3. Relative paths are resolved by the caller before reaching this function.
18
+ async function resolve(uri: string, fromPath: string, spineRoot?: string): Promise<string> {
19
+
20
+ // Scoped package: @scope/package/subpath
21
+ if (uri.startsWith('@') && spineRoot) {
22
+ const match = uri.match(/^@([^/]+)\/([^/]+)(?:\/(.+))?$/)
23
+ if (match) {
24
+ const [, scope, pkg, subpath] = match
25
+
26
+ if (subpath) {
27
+ // @scope/package/path → spineRoot/scope/packages/package/path.ts
28
+ const fsPath = join(spineRoot, scope, 'packages', pkg, subpath + '.ts')
29
+ try {
30
+ await stat(fsPath)
31
+ return fsPath
32
+ } catch {
33
+ // Try without .ts extension (already has extension)
34
+ try {
35
+ await stat(join(spineRoot, scope, 'packages', pkg, subpath))
36
+ return join(spineRoot, scope, 'packages', pkg, subpath)
37
+ } catch {
38
+ // Fall through to package.json exports resolution
39
+ }
40
+ }
41
+ }
42
+
43
+ // Try resolving via package.json exports
44
+ const packageDir = join(spineRoot, scope, 'packages', pkg)
45
+ try {
46
+ const packageJsonPath = join(packageDir, 'package.json')
47
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
48
+
49
+ if (subpath && packageJson.exports) {
50
+ // Look for matching export: "./" + subpath or "./" + subpath + ".ts"
51
+ const exportKey = './' + subpath
52
+ const exportValue = packageJson.exports[exportKey]
53
+ if (typeof exportValue === 'string') {
54
+ return pathResolve(packageDir, exportValue)
55
+ }
56
+ } else if (!subpath && packageJson.exports?.['.']) {
57
+ const mainExport = packageJson.exports['.']
58
+ if (typeof mainExport === 'string') {
59
+ return pathResolve(packageDir, mainExport)
60
+ }
61
+ } else if (!subpath && packageJson.main) {
62
+ return pathResolve(packageDir, packageJson.main)
63
+ }
64
+ } catch {
65
+ // package.json doesn't exist or is invalid
66
+ }
67
+
68
+ // Traverse up from spineRoot looking for the package:
69
+ // 1. Check if current dir IS the package (self-package)
70
+ // 2. Check node_modules/@scope/pkg at each level
71
+ let dir = spineRoot
72
+ while (true) {
73
+ // Check if this directory's package.json matches the requested package
74
+ try {
75
+ const pjPath = join(dir, 'package.json')
76
+ const pj = JSON.parse(await readFile(pjPath, 'utf-8'))
77
+ if (pj.name === `@${scope}/${pkg}`) {
78
+ if (subpath) {
79
+ if (pj.exports) {
80
+ const exportKey = './' + subpath
81
+ const exportValue = pj.exports[exportKey]
82
+ if (typeof exportValue === 'string') {
83
+ return pathResolve(dir, exportValue)
84
+ }
85
+ }
86
+ const fsPath = join(dir, subpath + '.ts')
87
+ try { await stat(fsPath); return fsPath } catch { }
88
+ try { await stat(join(dir, subpath)); return join(dir, subpath) } catch { }
89
+ } else if (pj.exports?.['.']) {
90
+ const mainExport = pj.exports['.']
91
+ if (typeof mainExport === 'string') {
92
+ return pathResolve(dir, mainExport)
93
+ }
94
+ } else if (pj.main) {
95
+ return pathResolve(dir, pj.main)
96
+ }
97
+ }
98
+ } catch { }
99
+
100
+ // Check node_modules/@scope/pkg
101
+ const nmCandidate = join(dir, 'node_modules', `@${scope}`, pkg)
102
+ try {
103
+ const nmPjPath = join(nmCandidate, 'package.json')
104
+ const nmPj = JSON.parse(await readFile(nmPjPath, 'utf-8'))
105
+ if (subpath) {
106
+ if (nmPj.exports) {
107
+ const exportKey = './' + subpath
108
+ const exportValue = nmPj.exports[exportKey]
109
+ if (typeof exportValue === 'string') {
110
+ return pathResolve(nmCandidate, exportValue)
111
+ }
112
+ }
113
+ const fsPath = join(nmCandidate, subpath + '.ts')
114
+ try { await stat(fsPath); return fsPath } catch { }
115
+ try { await stat(join(nmCandidate, subpath)); return join(nmCandidate, subpath) } catch { }
116
+ } else if (nmPj.exports?.['.']) {
117
+ const mainExport = nmPj.exports['.']
118
+ if (typeof mainExport === 'string') {
119
+ return pathResolve(nmCandidate, mainExport)
120
+ }
121
+ } else if (nmPj.main) {
122
+ return pathResolve(nmCandidate, nmPj.main)
123
+ }
124
+ } catch { }
125
+
126
+ const parent = dirname(dir)
127
+ if (parent === dir) break
128
+ dir = parent
129
+ }
130
+
131
+ // Also traverse up from fromPath (the importing file) checking:
132
+ // 1. If current dir IS the package (self-package resolution)
133
+ // 2. node_modules/@scope/pkg at each level
134
+ let fromDir = dirname(fromPath)
135
+ while (true) {
136
+ // Check if this directory's package.json matches the requested package
137
+ try {
138
+ const pjPath = join(fromDir, 'package.json')
139
+ const pj = JSON.parse(await readFile(pjPath, 'utf-8'))
140
+ if (pj.name === `@${scope}/${pkg}`) {
141
+ if (subpath) {
142
+ if (pj.exports) {
143
+ const exportKey = './' + subpath
144
+ const exportValue = pj.exports[exportKey]
145
+ if (typeof exportValue === 'string') {
146
+ return pathResolve(fromDir, exportValue)
147
+ }
148
+ }
149
+ const fsPath = join(fromDir, subpath + '.ts')
150
+ try { await stat(fsPath); return fsPath } catch { }
151
+ try { await stat(join(fromDir, subpath)); return join(fromDir, subpath) } catch { }
152
+ } else if (pj.exports?.['.']) {
153
+ const mainExport = pj.exports['.']
154
+ if (typeof mainExport === 'string') {
155
+ return pathResolve(fromDir, mainExport)
156
+ }
157
+ } else if (pj.main) {
158
+ return pathResolve(fromDir, pj.main)
159
+ }
160
+ }
161
+ } catch { }
162
+
163
+ const nmCandidate = join(fromDir, 'node_modules', `@${scope}`, pkg)
164
+ try {
165
+ const nmPjPath = join(nmCandidate, 'package.json')
166
+ const nmPj = JSON.parse(await readFile(nmPjPath, 'utf-8'))
167
+ if (subpath) {
168
+ if (nmPj.exports) {
169
+ const exportKey = './' + subpath
170
+ const exportValue = nmPj.exports[exportKey]
171
+ if (typeof exportValue === 'string') {
172
+ return pathResolve(nmCandidate, exportValue)
173
+ }
174
+ }
175
+ const fsPath = join(nmCandidate, subpath + '.ts')
176
+ try { await stat(fsPath); return fsPath } catch { }
177
+ try { await stat(join(nmCandidate, subpath)); return join(nmCandidate, subpath) } catch { }
178
+ } else if (nmPj.exports?.['.']) {
179
+ const mainExport = nmPj.exports['.']
180
+ if (typeof mainExport === 'string') {
181
+ return pathResolve(nmCandidate, mainExport)
182
+ }
183
+ } else if (nmPj.main) {
184
+ return pathResolve(nmCandidate, nmPj.main)
185
+ }
186
+ } catch { }
187
+
188
+ const parent = dirname(fromDir)
189
+ if (parent === fromDir) break
190
+ fromDir = parent
191
+ }
192
+ }
193
+ }
194
+
195
+ // Absolute path — probe extensions
196
+ if (uri.startsWith('/')) {
197
+ // Try exact path first
198
+ try {
199
+ const s = await stat(uri)
200
+ if (s.isFile()) return uri
201
+ if (s.isDirectory()) {
202
+ for (const idx of ['index.ts', 'index.js']) {
203
+ try { await stat(join(uri, idx)); return join(uri, idx) } catch { }
204
+ }
205
+ }
206
+ } catch { }
207
+ // Try with extensions
208
+ for (const ext of ['.ts', '.js', '.mjs']) {
209
+ try { await stat(uri + ext); return uri + ext } catch { }
210
+ }
211
+ }
212
+
213
+ // Non-scoped bare specifier — try node_modules resolution from fromPath
214
+ if (!uri.startsWith('.') && !uri.startsWith('/')) {
215
+ // Walk up from fromPath looking for node_modules
216
+ let dir = dirname(fromPath)
217
+ while (true) {
218
+ const candidate = join(dir, 'node_modules', uri)
219
+ try {
220
+ const s = await stat(candidate)
221
+ if (s.isDirectory()) {
222
+ // Try package.json main/exports
223
+ try {
224
+ const pj = JSON.parse(await readFile(join(candidate, 'package.json'), 'utf-8'))
225
+ if (pj.main) return pathResolve(candidate, pj.main)
226
+ } catch { }
227
+ return join(candidate, 'index.js')
228
+ }
229
+ return candidate
230
+ } catch { }
231
+ // Try with extensions
232
+ for (const ext of ['.ts', '.js', '.mjs']) {
233
+ try {
234
+ await stat(candidate + ext)
235
+ return candidate + ext
236
+ } catch { }
237
+ }
238
+ const parent = dirname(dir)
239
+ if (parent === dir) break
240
+ dir = parent
241
+ }
242
+ }
243
+
244
+ throw new Error(`[encapsulate resolve] Cannot resolve '${uri}' from '${fromPath}' (spineRoot: ${spineRoot})`)
245
+ }
246
+
247
+
248
+ export async function CapsuleSpineFactory({
249
+ spineFilesystemRoot,
250
+ capsuleModuleProjectionRoot,
251
+ capsuleModuleProjectionPackage,
252
+ staticAnalysisEnabled = true,
253
+ onMembraneEvent,
254
+ enableCallerStackInference = false,
255
+ spineContracts,
256
+ timing: timingParam
257
+ }: {
258
+ spineFilesystemRoot: string,
259
+ capsuleModuleProjectionRoot?: string,
260
+ capsuleModuleProjectionPackage?: string,
261
+ staticAnalysisEnabled?: boolean,
262
+ onMembraneEvent?: (event: any) => void,
263
+ enableCallerStackInference?: boolean,
264
+ spineContracts: Record<string, any>,
265
+ timing?: { record: (step: string) => void, recordMajor: (step: string) => void, chalk?: any }
266
+ }) {
267
+
268
+ if (capsuleModuleProjectionRoot) capsuleModuleProjectionRoot = capsuleModuleProjectionRoot.replace(/^file:\/\//, '')
269
+ if (spineFilesystemRoot) spineFilesystemRoot = spineFilesystemRoot.replace(/^file:\/\//, '')
270
+
271
+ const timing = timingParam
272
+
273
+ timing?.recordMajor('CAPSULE SPINE FACTORY: INITIALIZATION')
274
+
275
+ const SingletonRegistry = () => {
276
+ const registry = new Map<string, Promise<any>>()
277
+
278
+ return {
279
+ async ensure(id: string, createHandler: () => Promise<any>) {
280
+ if (!registry.has(id)) {
281
+ registry.set(id, createHandler())
282
+ }
283
+ return registry.get(id)!
284
+ }
285
+ }
286
+ }
287
+
288
+ const registry = SingletonRegistry()
289
+
290
+ const spineContractInstances: {
291
+ encapsulation: Record<string, any>,
292
+ runtime: Record<string, any>
293
+ } = {
294
+ encapsulation: {},
295
+ runtime: {}
296
+ }
297
+
298
+ const sourceSpine: { encapsulate?: any } = {}
299
+ const commonSpineContractOpts = {
300
+ spineFilesystemRoot,
301
+ resolve: async (uri: string, parentFilepath: string) => {
302
+ // For relative paths, join with parent directory first
303
+ if (/^\.\.?\//.test(uri)) {
304
+ return await resolve(join(parentFilepath, '..', uri), spineFilesystemRoot, spineFilesystemRoot)
305
+ }
306
+ // For absolute/package paths, use custom resolve with spine root
307
+ return await resolve(uri, parentFilepath, spineFilesystemRoot)
308
+ },
309
+ importCapsule: (() => {
310
+ return async (filepath: string) => {
311
+ const shortPath = filepath.replace(/^.*\/genesis\//, '')
312
+
313
+ timing?.record(`importCapsule: Called for ${shortPath}`)
314
+ const result = await registry.ensure(filepath, async () => {
315
+ timing?.recordMajor(`importCapsule: Starting import for ${shortPath}`)
316
+ const importStart = Date.now()
317
+ const exports = await import(filepath)
318
+ const importDuration = Date.now() - importStart
319
+ timing?.recordMajor(`importCapsule: import() took ${importDuration}ms for ${shortPath}`)
320
+
321
+ if (importDuration > 10) {
322
+ if (timing) {
323
+ console.log(timing.chalk.red(`\n⚠️ WARNING: Slow module load detected!`))
324
+ console.log(timing.chalk.red(` Module: ${filepath}`))
325
+ console.log(timing.chalk.red(` Load time: ${importDuration}ms`))
326
+ console.log(timing.chalk.red(` Consider using dynamic imports to load heavy dependencies only when needed.\n`))
327
+ }
328
+ }
329
+
330
+ if (typeof exports.capsule !== 'function') throw new Error(`Module at '${filepath}' does not export 'capsule'!`)
331
+
332
+ const capsuleStart = Date.now()
333
+ const capsule = await exports.capsule({
334
+ encapsulate: sourceSpine.encapsulate,
335
+ CapsulePropertyTypes,
336
+ makeImportStack
337
+ })
338
+ const capsuleDuration = Date.now() - capsuleStart
339
+ timing?.recordMajor(`importCapsule: exports.capsule() took ${capsuleDuration}ms for ${shortPath}`)
340
+
341
+ timing?.record(`importCapsule: Returning result for ${shortPath}`)
342
+
343
+ return capsule
344
+ })
345
+ return result
346
+ }
347
+ })(),
348
+ encapsulateOpts: {
349
+ CapsulePropertyTypes
350
+ }
351
+ }
352
+
353
+ timing?.recordMajor('SPINE CONTRACTS: INITIALIZATION')
354
+
355
+ for (const spineContractUri in spineContracts) {
356
+ spineContractInstances.encapsulation[spineContractUri] = spineContracts[spineContractUri]({
357
+ ...commonSpineContractOpts,
358
+ freezeCapsule: async ({ spineContractUri, capsule }: { spineContractUri: string, capsule: any }): Promise<any> => {
359
+
360
+ if (!projector) {
361
+ throw new Error('capsuleModuleProjectionRoot must be provided to enable freezing')
362
+ }
363
+
364
+ let snapshotValues = {}
365
+
366
+ // Create a new set per freezeCapsule call to track circular dependencies within this projection tree
367
+ const projectingCapsules = new Set<string>()
368
+
369
+ const projected = await projector.projectCapsule({
370
+ capsule,
371
+ capsules,
372
+ snapshotValues,
373
+ spineContractUri,
374
+ projectingCapsules
375
+ })
376
+
377
+ return snapshotValues
378
+ }
379
+ })
380
+ spineContractInstances.runtime[spineContractUri] = spineContracts[spineContractUri]({
381
+ ...commonSpineContractOpts,
382
+ enableCallerStackInference,
383
+ onMembraneEvent,
384
+ })
385
+ }
386
+
387
+ timing?.recordMajor('CAPSULE MODULE PROJECTOR: INITIALIZATION')
388
+
389
+ const projector = capsuleModuleProjectionRoot ? CapsuleModuleProjector({
390
+ spineStore: {
391
+ writeFile: async (filepath: string, content: string) => {
392
+ filepath = join(spineFilesystemRoot, filepath)
393
+ await mkdir(dirname(filepath), { recursive: true })
394
+ await writeFile(filepath, content, 'utf-8')
395
+ },
396
+ getStats: async (filepath: string) => {
397
+ filepath = join(spineFilesystemRoot, filepath)
398
+ try {
399
+ const stats = await stat(filepath)
400
+ return { mtime: stats.mtime }
401
+ } catch (error) {
402
+ return null
403
+ }
404
+ },
405
+ },
406
+ projectionStore: {
407
+ writeFile: async (filepath: string, content: string) => {
408
+ filepath = join(capsuleModuleProjectionRoot, filepath)
409
+ await mkdir(dirname(filepath), { recursive: true })
410
+ await writeFile(filepath, content, 'utf-8')
411
+ },
412
+ getStats: async (filepath: string) => {
413
+ filepath = join(capsuleModuleProjectionRoot, filepath)
414
+ try {
415
+ const stats = await stat(filepath)
416
+ return { mtime: stats.mtime }
417
+ } catch (error) {
418
+ return null
419
+ }
420
+ },
421
+ },
422
+ projectionCacheStore: {
423
+ writeFile: async (filepath: string, content: string) => {
424
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
425
+ await mkdir(dirname(filepath), { recursive: true })
426
+ await writeFile(filepath, content, 'utf-8')
427
+ },
428
+ readFile: async (filepath: string) => {
429
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
430
+ return readFile(filepath, 'utf-8')
431
+ },
432
+ getStats: async (filepath: string) => {
433
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
434
+ try {
435
+ const stats = await stat(filepath)
436
+ return { mtime: stats.mtime }
437
+ } catch (error) {
438
+ return null
439
+ }
440
+ },
441
+ },
442
+ spineFilesystemRoot,
443
+ capsuleModuleProjectionPackage,
444
+ timing
445
+ }) : undefined
446
+
447
+ timing?.recordMajor('SPINE: INITIALIZATION')
448
+
449
+ let { encapsulate, freeze, capsules } = await Spine({
450
+ spineFilesystemRoot,
451
+ timing,
452
+ staticAnalyzer: staticAnalysisEnabled ? StaticAnalyzer({
453
+ timing,
454
+ cacheStore: {
455
+ writeFile: async (filepath: string, content: string) => {
456
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
457
+ await mkdir(dirname(filepath), { recursive: true })
458
+ await writeFile(filepath, content, 'utf-8')
459
+ },
460
+ readFile: async (filepath: string) => {
461
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
462
+ return readFile(filepath, 'utf-8')
463
+ },
464
+ getStats: async (filepath: string) => {
465
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
466
+ try {
467
+ const stats = await stat(filepath)
468
+ return { mtime: stats.mtime }
469
+ } catch (error) {
470
+ // File doesn't exist
471
+ return null
472
+ }
473
+ },
474
+ },
475
+ spineStore: {
476
+ getStats: async (filepath: string) => {
477
+ filepath = join(spineFilesystemRoot, filepath)
478
+ try {
479
+ const stats = await stat(filepath)
480
+ return { mtime: stats.mtime }
481
+ } catch (error) {
482
+ // File doesn't exist
483
+ return null
484
+ }
485
+ },
486
+ },
487
+ }) : undefined,
488
+ spineContracts: spineContractInstances.encapsulation
489
+ })
490
+ sourceSpine.encapsulate = encapsulate
491
+
492
+ timing?.recordMajor('SPINE RUNTIME: INITIALIZATION')
493
+
494
+ let { run } = await SpineRuntime({
495
+ spineFilesystemRoot,
496
+ spineContracts: spineContractInstances.runtime,
497
+ capsules
498
+ })
499
+
500
+ timing?.recordMajor('CAPSULE SPINE FACTORY: READY')
501
+
502
+ const loadCapsule = async ({ capsuleSnapshot }: { capsuleSourceLineRef: string, capsuleSnapshot: any }) => {
503
+
504
+ if (!capsuleModuleProjectionRoot) {
505
+ throw new Error('capsuleModuleProjectionRoot must be provided to enable dynamic loading of capsules')
506
+ }
507
+
508
+ let filepath = capsuleSnapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.projectedCapsuleFilepath
509
+
510
+ if (!filepath) throw new Error(`Cannot load capsule. No 'filepath' found at 'spineContracts["#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0"]["#@stream44.studio/encapsulate/structs/Capsule"].projectedCapsuleFilepath'!`)
511
+
512
+ const { capsule } = await import(join(capsuleModuleProjectionRoot, filepath))
513
+
514
+ return capsule
515
+ }
516
+
517
+ return {
518
+ commonSpineContractOpts,
519
+ CapsulePropertyTypes,
520
+ makeImportStack,
521
+ encapsulate,
522
+ run,
523
+ freeze,
524
+ loadCapsule,
525
+ spineContractInstances, // Expose for testing
526
+ hoistSnapshot: async ({ snapshot }: { snapshot: any }) => {
527
+
528
+ timing?.recordMajor('HOIST SNAPSHOT: START')
529
+
530
+ const result = await SpineRuntime({
531
+ snapshot,
532
+ spineContracts: spineContractInstances.runtime,
533
+ loadCapsule
534
+ })
535
+
536
+ timing?.recordMajor('HOIST SNAPSHOT: COMPLETE')
537
+
538
+ return result
539
+ }
540
+ }
541
+ }
@@ -0,0 +1,26 @@
1
+
2
+ import chalk from 'chalk';
3
+
4
+ export function TimingObserver({ startTime }: { startTime: number }) {
5
+ let lastTime = startTime
6
+
7
+ return {
8
+ chalk,
9
+ record: (step: string) => {
10
+ const now = Date.now()
11
+ const diff = now - lastTime
12
+ lastTime = now
13
+
14
+ const line = `[+${diff}ms] ${step}`
15
+ console.log(diff > 10 ? chalk.red(line) : line)
16
+ },
17
+ recordMajor: (step: string) => {
18
+ const now = Date.now()
19
+ const diff = now - lastTime
20
+ lastTime = now
21
+
22
+ const line = `[+${diff}ms] ${step}`
23
+ console.log(diff > 10 ? chalk.red(line) : chalk.cyan(line))
24
+ }
25
+ }
26
+ }