@stream44.studio/encapsulate 0.2.0-rc.2 → 0.2.0-rc.4

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.

Potentially problematic release.


This version of @stream44.studio/encapsulate might be problematic. Click here for more details.

@@ -22,25 +22,25 @@ const ENCAPSULATE_MODULE_EXPORTS = new Set([
22
22
  async function constructNpmUri(absoluteFilepath: string, spineRoot: string): Promise<string | null> {
23
23
  let currentDir = dirname(absoluteFilepath)
24
24
  const maxDepth = 20 // Prevent infinite loops
25
-
25
+
26
26
  for (let i = 0; i < maxDepth; i++) {
27
27
  const packageJsonPath = join(currentDir, 'package.json')
28
-
28
+
29
29
  try {
30
30
  await stat(packageJsonPath)
31
31
  // Found package.json, read it
32
32
  const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
33
33
  const packageName = packageJson.name
34
-
34
+
35
35
  if (!packageName) {
36
36
  // No name in package.json, continue searching
37
37
  currentDir = dirname(currentDir)
38
38
  continue
39
39
  }
40
-
40
+
41
41
  // Get the relative path from the package root to the file
42
42
  const relativeFromPackage = relative(currentDir, absoluteFilepath)
43
-
43
+
44
44
  // Construct npm URI: packageName/relativePath
45
45
  return `${packageName}/${relativeFromPackage}`
46
46
  } catch (error) {
@@ -53,7 +53,7 @@ async function constructNpmUri(absoluteFilepath: string, spineRoot: string): Pro
53
53
  currentDir = parentDir
54
54
  }
55
55
  }
56
-
56
+
57
57
  return null
58
58
  }
59
59
 
@@ -63,6 +63,12 @@ const MODULE_GLOBAL_BUILTINS = new Set([
63
63
 
64
64
  'process',
65
65
 
66
+ // Bun runtime
67
+ 'Bun',
68
+
69
+ // Node.js Buffer
70
+ 'Buffer',
71
+
66
72
  // Console API
67
73
  'console',
68
74
 
@@ -186,7 +192,7 @@ export function StaticAnalyzer({
186
192
  parseModule: async ({ spineOptions, encapsulateOptions }: { spineOptions: any, encapsulateOptions: any }) => {
187
193
 
188
194
  const moduleFilepath = join(spineOptions.spineFilesystemRoot, encapsulateOptions.moduleFilepath)
189
-
195
+
190
196
  // Determine the cache file path based on whether the module is external or internal
191
197
  let cacheFilePath: string
192
198
  if (encapsulateOptions.moduleFilepath.startsWith('../')) {
@@ -202,7 +208,7 @@ export function StaticAnalyzer({
202
208
  // Internal module - use relative path as-is
203
209
  cacheFilePath = encapsulateOptions.moduleFilepath
204
210
  }
205
-
211
+
206
212
  const capsuleSourceLineRef = `${cacheFilePath}:${encapsulateOptions.importStackLine}`
207
213
 
208
214
  // Try to load from cache first
@@ -306,16 +312,16 @@ export function StaticAnalyzer({
306
312
 
307
313
  // Construct npm URI for the module - try for all modules
308
314
  let moduleUri: string | null = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
309
-
315
+
310
316
  // If npm URI construction failed, fall back to moduleFilepath
311
317
  if (!moduleUri) {
312
318
  moduleUri = encapsulateOptions.moduleFilepath
313
319
  }
314
-
320
+
315
321
  // Strip file extension from URI
316
322
  const moduleUriWithoutExt = moduleUri.replace(/\.(ts|tsx|js|jsx)$/, '')
317
323
  const capsuleSourceUriLineRef = `${moduleUriWithoutExt}:${encapsulateOptions.importStackLine}`
318
-
324
+
319
325
  // Store moduleUri without extension
320
326
  moduleUri = moduleUriWithoutExt
321
327
 
@@ -371,6 +377,21 @@ export function StaticAnalyzer({
371
377
  const optionsEndPos = sourceFile.getLineAndCharacterOfPosition(optionsObject.getEnd())
372
378
  cst.source.optionsStartLine = optionsStartPos.line + 1
373
379
  cst.source.optionsEndLine = optionsEndPos.line + 1
380
+
381
+ // Extract extendsCapsule option if present
382
+ for (const prop of optionsObject.properties) {
383
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'extendsCapsule') {
384
+ // Check if it's a string literal (relative path or npm URI)
385
+ if (ts.isStringLiteral(prop.initializer)) {
386
+ cst.source.extendsCapsule = prop.initializer.text
387
+ }
388
+ // Check if it's an identifier (capsule variable reference)
389
+ else if (ts.isIdentifier(prop.initializer)) {
390
+ cst.source.extendsCapsule = prop.initializer.text
391
+ }
392
+ break
393
+ }
394
+ }
374
395
  }
375
396
 
376
397
  // Parse spineContract definitions (e.g., '$spineContract1': { ... })
@@ -423,6 +444,15 @@ export function StaticAnalyzer({
423
444
  }
424
445
  }
425
446
 
447
+ // Check for 'as' property at the property contract level
448
+ for (const contractProp of propValue.properties) {
449
+ if (ts.isPropertyAssignment(contractProp) && ts.isIdentifier(contractProp.name) && contractProp.name.text === 'as') {
450
+ if (ts.isStringLiteral(contractProp.initializer)) {
451
+ spineContractDef.properties[propName].as = contractProp.initializer.text
452
+ }
453
+ }
454
+ }
455
+
426
456
  // Parse properties within the property contract
427
457
  for (const contractProp of propValue.properties) {
428
458
  if (ts.isPropertyAssignment(contractProp) && (ts.isIdentifier(contractProp.name) || ts.isStringLiteral(contractProp.name))) {
@@ -533,7 +563,9 @@ export function StaticAnalyzer({
533
563
 
534
564
  // Add a dynamic mapping for each non-default property contract
535
565
  for (const propContractUri of nonDefaultContracts) {
536
- const contractKey = '#' + propContractUri.substring(1)
566
+ // Check if 'as' alias is defined for this property contract
567
+ const aliasName = spineContract.properties[propContractUri]?.as
568
+ const contractKey = aliasName || ('#' + propContractUri.substring(1))
537
569
  spineContract.properties['#'].properties[contractKey] = {
538
570
  declarationLine: -1,
539
571
  definitionStartLine: -1,
@@ -541,7 +573,8 @@ export function StaticAnalyzer({
541
573
  type: 'CapsulePropertyTypes.Mapping',
542
574
  valueType: 'string',
543
575
  valueExpression: `"${propContractUri.substring(1)}"`,
544
- propertyContractDelegate: propContractUri
576
+ propertyContractDelegate: propContractUri,
577
+ as: aliasName
545
578
  }
546
579
  }
547
580
  }
@@ -777,7 +810,7 @@ function extractFunctionSignature(fn: ts.FunctionExpression | ts.ArrowFunction,
777
810
  return `(${params.join(', ')}) => ${returnType}`
778
811
  }
779
812
 
780
- // Extract module-local functions that are self-contained
813
+ // Extract module-local functions and variables that are self-contained
781
814
  function extractModuleLocalCode(
782
815
  ambientReferences: Record<string, any>,
783
816
  sourceFile: ts.SourceFile,
@@ -787,12 +820,21 @@ function extractModuleLocalCode(
787
820
  ): Record<string, string> {
788
821
  const moduleLocalCode: Record<string, string> = {}
789
822
  const moduleLocalFunctions = new Map<string, ts.FunctionDeclaration>()
823
+ const moduleLocalVariables = new Map<string, ts.VariableDeclaration>()
790
824
 
791
- // First, collect all top-level function declarations in the module (including async functions)
825
+ // First, collect all top-level function declarations and variable declarations in the module
792
826
  for (const statement of sourceFile.statements) {
793
827
  if (ts.isFunctionDeclaration(statement) && statement.name) {
794
828
  moduleLocalFunctions.set(statement.name.text, statement)
795
829
  }
830
+ // Collect module-level variable declarations
831
+ if (ts.isVariableStatement(statement)) {
832
+ for (const decl of statement.declarationList.declarations) {
833
+ if (ts.isIdentifier(decl.name)) {
834
+ moduleLocalVariables.set(decl.name.text, decl)
835
+ }
836
+ }
837
+ }
796
838
  }
797
839
 
798
840
  // Also collect functions from the local scope around the call node
@@ -813,7 +855,7 @@ function extractModuleLocalCode(
813
855
  }
814
856
  }
815
857
 
816
- // Check each ambient reference to see if it's a module-local function
858
+ // Check each ambient reference to see if it's a module-local function or variable
817
859
  for (const [name, ref] of Object.entries(ambientReferences)) {
818
860
  const refTyped = ref as any
819
861
 
@@ -824,45 +866,68 @@ function extractModuleLocalCode(
824
866
 
825
867
  // Check if this identifier refers to a module-local function
826
868
  const funcDecl = moduleLocalFunctions.get(name)
827
- if (!funcDecl) continue
828
-
829
- // Analyze the function to see if it's self-contained
830
- const dependencies = new Set<string>()
831
- const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
832
-
833
- if (isContained) {
834
- // Mark this as module-local in ambient references
835
- refTyped.type = 'module-local'
836
-
837
- // Collect the function code and all its dependencies
838
- const collectedCode: string[] = []
839
- const processed = new Set<string>()
840
-
841
- function collectFunction(fnName: string) {
842
- if (processed.has(fnName)) return
843
- processed.add(fnName)
844
-
845
- const fn = moduleLocalFunctions.get(fnName)
846
- if (fn) {
847
- const fnCode = fn.getText(sourceFile)
848
- collectedCode.push(fnCode)
849
- // Also add each function as a separate entry in moduleLocalCode
850
- if (!moduleLocalCode[fnName]) {
851
- moduleLocalCode[fnName] = fnCode
869
+ if (funcDecl) {
870
+ // Analyze the function to see if it's self-contained
871
+ const dependencies = new Set<string>()
872
+ const isContained = analyzeFunctionDependencies(funcDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, dependencies)
873
+
874
+ if (isContained) {
875
+ // Mark this as module-local in ambient references
876
+ refTyped.type = 'module-local'
877
+
878
+ // Collect the function code and all its dependencies
879
+ const collectedCode: string[] = []
880
+ const processed = new Set<string>()
881
+
882
+ function collectFunction(fnName: string) {
883
+ if (processed.has(fnName)) return
884
+ processed.add(fnName)
885
+
886
+ const fn = moduleLocalFunctions.get(fnName)
887
+ if (fn) {
888
+ const fnCode = fn.getText(sourceFile)
889
+ collectedCode.push(fnCode)
890
+ // Also add each function as a separate entry in moduleLocalCode
891
+ if (!moduleLocalCode[fnName]) {
892
+ moduleLocalCode[fnName] = fnCode
893
+ }
852
894
  }
853
895
  }
854
- }
855
896
 
856
- // Collect the main function
857
- collectFunction(name)
897
+ // Collect the main function
898
+ collectFunction(name)
899
+
900
+ // Collect all dependencies
901
+ for (const dep of dependencies) {
902
+ collectFunction(dep)
903
+ }
858
904
 
859
- // Collect all dependencies
860
- for (const dep of dependencies) {
861
- collectFunction(dep)
905
+ // Store the collected code (main function with all dependencies)
906
+ moduleLocalCode[name] = collectedCode.join('\n\n')
862
907
  }
908
+ continue
909
+ }
863
910
 
864
- // Store the collected code (main function with all dependencies)
865
- moduleLocalCode[name] = collectedCode.join('\n\n')
911
+ // Check if this identifier refers to a module-local variable
912
+ const varDecl = moduleLocalVariables.get(name)
913
+ if (varDecl) {
914
+ // Analyze the variable to see if it's self-contained
915
+ const varDependencies = analyzeVariableDependencies(varDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, moduleLocalVariables)
916
+
917
+ if (varDependencies.isContained) {
918
+ // Mark this as module-local in ambient references
919
+ refTyped.type = 'module-local'
920
+
921
+ // Get the variable declaration code
922
+ // We need to get the full variable statement (const/let/var KEYS_DIR = ...)
923
+ const varStatement = varDecl.parent?.parent
924
+ if (varStatement && ts.isVariableStatement(varStatement)) {
925
+ moduleLocalCode[name] = varStatement.getText(sourceFile)
926
+ } else {
927
+ // Fallback to just the declaration
928
+ moduleLocalCode[name] = varDecl.getText(sourceFile)
929
+ }
930
+ }
866
931
  }
867
932
  }
868
933
 
@@ -961,6 +1026,11 @@ function analyzeFunctionDependencies(
961
1026
 
962
1027
  // Second pass: check for external dependencies
963
1028
  function visit(node: ts.Node, currentScope: Set<string> = localIdentifiers) {
1029
+ // Skip type nodes to avoid false positives from type annotations
1030
+ if (ts.isTypeNode(node)) {
1031
+ return
1032
+ }
1033
+
964
1034
  // Use the appropriate scope for nested functions
965
1035
  if (nestedFunctionScopes.has(node)) {
966
1036
  currentScope = nestedFunctionScopes.get(node)!
@@ -1028,6 +1098,92 @@ function analyzeFunctionDependencies(
1028
1098
  return isContained
1029
1099
  }
1030
1100
 
1101
+ // Analyze if a variable declaration is self-contained (only depends on imports and builtins)
1102
+ // Returns whether it's contained and the import dependencies needed
1103
+ function analyzeVariableDependencies(
1104
+ varDecl: ts.VariableDeclaration,
1105
+ sourceFile: ts.SourceFile,
1106
+ importMap: Map<string, { importSpecifier: string, moduleUri: string }>,
1107
+ assignmentMap: Map<string, { importSpecifier: string, moduleUri: string }>,
1108
+ moduleLocalFunctions: Map<string, ts.FunctionDeclaration>,
1109
+ moduleLocalVariables: Map<string, ts.VariableDeclaration>
1110
+ ): { isContained: boolean, importDependencies: Map<string, { importSpecifier: string, moduleUri: string }> } {
1111
+ const importDependencies = new Map<string, { importSpecifier: string, moduleUri: string }>()
1112
+ let isContained = true
1113
+
1114
+ if (!varDecl.initializer) {
1115
+ // No initializer means it's just a declaration, treat as contained
1116
+ return { isContained: true, importDependencies }
1117
+ }
1118
+
1119
+ function visit(node: ts.Node) {
1120
+ // Skip type nodes
1121
+ if (ts.isTypeNode(node)) {
1122
+ return
1123
+ }
1124
+
1125
+ if (ts.isIdentifier(node)) {
1126
+ const identifierName = node.text
1127
+
1128
+ // Skip special keywords
1129
+ if (identifierName === 'this' || identifierName === 'undefined' || identifierName === 'null') {
1130
+ return
1131
+ }
1132
+
1133
+ // Skip property access names
1134
+ const parent = node.parent
1135
+ if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) {
1136
+ return
1137
+ }
1138
+
1139
+ // Skip property names in object literals
1140
+ if (parent && ts.isPropertyAssignment(parent) && parent.name === node) {
1141
+ return
1142
+ }
1143
+
1144
+ // Check if it's an import - track as dependency
1145
+ const importInfo = importMap.get(identifierName)
1146
+ if (importInfo) {
1147
+ importDependencies.set(identifierName, importInfo)
1148
+ return
1149
+ }
1150
+
1151
+ // Check if it's an assignment from import
1152
+ const assignmentInfo = assignmentMap.get(identifierName)
1153
+ if (assignmentInfo) {
1154
+ importDependencies.set(identifierName, assignmentInfo)
1155
+ return
1156
+ }
1157
+
1158
+ // Check if it's a module-global builtin - allowed
1159
+ if (MODULE_GLOBAL_BUILTINS.has(identifierName)) {
1160
+ return
1161
+ }
1162
+
1163
+ // Check if it's another module-local variable - recursively analyze
1164
+ if (moduleLocalVariables.has(identifierName)) {
1165
+ // For now, allow references to other module-local variables
1166
+ // A more complete implementation would recursively analyze
1167
+ return
1168
+ }
1169
+
1170
+ // Check if it's a module-local function - allowed
1171
+ if (moduleLocalFunctions.has(identifierName)) {
1172
+ return
1173
+ }
1174
+
1175
+ // Unknown external reference - not self-contained
1176
+ isContained = false
1177
+ }
1178
+
1179
+ ts.forEachChild(node, visit)
1180
+ }
1181
+
1182
+ visit(varDecl.initializer)
1183
+
1184
+ return { isContained, importDependencies }
1185
+ }
1186
+
1031
1187
  // Helper to extract binding identifiers for analysis
1032
1188
  function extractBindingIdentifiersForAnalysis(name: ts.BindingName, targetSet: Set<string>) {
1033
1189
  if (ts.isIdentifier(name)) {
@@ -1060,13 +1216,21 @@ function extractCapsuleAmbientReferences(
1060
1216
  const propertyNames = new Set<string>()
1061
1217
  const invocationParameters = new Set<string>()
1062
1218
  const moduleLocalFunctions = new Map<string, ts.FunctionDeclaration>()
1219
+ const moduleLocalVariables = new Map<string, ts.VariableDeclaration>()
1063
1220
 
1064
- // Collect module-local functions from both module top-level and local scope
1065
- // First, collect top-level module functions
1221
+ // Collect module-local functions and variables from module top-level
1066
1222
  for (const statement of sourceFile.statements) {
1067
1223
  if (ts.isFunctionDeclaration(statement) && statement.name) {
1068
1224
  moduleLocalFunctions.set(statement.name.text, statement)
1069
1225
  }
1226
+ // Collect module-level variable declarations
1227
+ if (ts.isVariableStatement(statement)) {
1228
+ for (const decl of statement.declarationList.declarations) {
1229
+ if (ts.isIdentifier(decl.name)) {
1230
+ moduleLocalVariables.set(decl.name.text, decl)
1231
+ }
1232
+ }
1233
+ }
1070
1234
  }
1071
1235
 
1072
1236
  // Find enclosing function and collect its parameters and local functions
@@ -1235,6 +1399,32 @@ function extractCapsuleAmbientReferences(
1235
1399
  }
1236
1400
  }
1237
1401
 
1402
+ // Check if it's a module-local variable (const/let/var at module level)
1403
+ const varDecl = moduleLocalVariables.get(identifierName)
1404
+ if (varDecl) {
1405
+ // Analyze the variable's initializer for dependencies
1406
+ const varDependencies = analyzeVariableDependencies(varDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, moduleLocalVariables)
1407
+
1408
+ if (varDependencies.isContained) {
1409
+ // Mark as module-local and add any import dependencies to ambientRefs
1410
+ ambientRefs[identifierName] = {
1411
+ type: 'module-local'
1412
+ }
1413
+
1414
+ // Add import dependencies from the variable's initializer
1415
+ for (const [depName, depInfo] of varDependencies.importDependencies) {
1416
+ if (!ambientRefs[depName]) {
1417
+ ambientRefs[depName] = {
1418
+ type: 'import',
1419
+ importSpecifier: depInfo.importSpecifier,
1420
+ moduleUri: depInfo.moduleUri
1421
+ }
1422
+ }
1423
+ }
1424
+ return
1425
+ }
1426
+ }
1427
+
1238
1428
  // This is a literal ambient reference
1239
1429
  // Check if the ambient reference is provided
1240
1430
  if (runtimeAmbientRefs && identifierName in runtimeAmbientRefs) {
@@ -1282,12 +1472,22 @@ function extractAndValidateAmbientReferences(
1282
1472
  ): Record<string, any> {
1283
1473
  // Build module-local functions map for checking
1284
1474
  const moduleLocalFunctions = new Map<string, ts.FunctionDeclaration>()
1475
+ // Build module-local variables map for checking (const/let/var declarations at module level)
1476
+ const moduleLocalVariables = new Map<string, ts.VariableDeclaration>()
1285
1477
 
1286
- // Collect top-level module functions
1478
+ // Collect top-level module functions and variables
1287
1479
  for (const statement of sourceFile.statements) {
1288
1480
  if (ts.isFunctionDeclaration(statement) && statement.name) {
1289
1481
  moduleLocalFunctions.set(statement.name.text, statement)
1290
1482
  }
1483
+ // Collect module-level variable declarations
1484
+ if (ts.isVariableStatement(statement)) {
1485
+ for (const decl of statement.declarationList.declarations) {
1486
+ if (ts.isIdentifier(decl.name)) {
1487
+ moduleLocalVariables.set(decl.name.text, decl)
1488
+ }
1489
+ }
1490
+ }
1291
1491
  }
1292
1492
 
1293
1493
  const ambientRefs: Record<string, any> = {}
@@ -1381,6 +1581,10 @@ function extractAndValidateAmbientReferences(
1381
1581
  // Track function declarations
1382
1582
  if (ts.isFunctionDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
1383
1583
  localIdentifiers.add(node.name.text)
1584
+ // Also track parameters from nested function declarations
1585
+ for (const param of node.parameters) {
1586
+ extractBindingIdentifiers(param.name)
1587
+ }
1384
1588
  }
1385
1589
 
1386
1590
  // Track named function expressions (e.g., function Counter() {})
@@ -1447,6 +1651,15 @@ function extractAndValidateAmbientReferences(
1447
1651
  return
1448
1652
  }
1449
1653
 
1654
+ // Skip if this is a capsule['#'] pattern - the capsule name reference
1655
+ // These references are resolved at encapsulation time and replaced with the actual string value
1656
+ if (parent && ts.isElementAccessExpression(parent) && parent.expression === node) {
1657
+ const arg = parent.argumentExpression
1658
+ if (arg && ts.isStringLiteral(arg) && arg.text === '#') {
1659
+ return
1660
+ }
1661
+ }
1662
+
1450
1663
  // Check if we already added this reference
1451
1664
  if (ambientRefs[identifierName]) {
1452
1665
  return
@@ -1500,6 +1713,32 @@ function extractAndValidateAmbientReferences(
1500
1713
  }
1501
1714
  }
1502
1715
 
1716
+ // Check if it's a module-local variable (const/let/var at module level)
1717
+ const varDecl = moduleLocalVariables.get(identifierName)
1718
+ if (varDecl) {
1719
+ // Analyze the variable's initializer for dependencies
1720
+ const varDependencies = analyzeVariableDependencies(varDecl, sourceFile, importMap, assignmentMap, moduleLocalFunctions, moduleLocalVariables)
1721
+
1722
+ if (varDependencies.isContained) {
1723
+ // Mark as module-local and add any import dependencies to ambientRefs
1724
+ ambientRefs[identifierName] = {
1725
+ type: 'module-local'
1726
+ }
1727
+
1728
+ // Add import dependencies from the variable's initializer
1729
+ for (const [depName, depInfo] of varDependencies.importDependencies) {
1730
+ if (!ambientRefs[depName]) {
1731
+ ambientRefs[depName] = {
1732
+ type: 'import',
1733
+ importSpecifier: depInfo.importSpecifier,
1734
+ moduleUri: depInfo.moduleUri
1735
+ }
1736
+ }
1737
+ }
1738
+ return
1739
+ }
1740
+ }
1741
+
1503
1742
  // Check if this is a JSX intrinsic element (like 'div', 'button', etc.) in a .tsx/.jsx file
1504
1743
  const fileName = sourceFile.fileName
1505
1744
  const isJsxFile = fileName.endsWith('.tsx') || fileName.endsWith('.jsx')