jslike 1.8.0 → 1.8.2

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/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  - **Production-Ready** - Handles files of any size, tested with 1000+ tests
8
8
  - **Full ES6+ JavaScript Support** - Classes, destructuring, template literals, spread operator, arrow functions
9
9
  - **Native JSX Support** - Parse and execute JSX without pre-transformation
10
+ - **TypeScript & TSX Module Support** - Execute JS-compatible TypeScript syntax directly from `.ts`, `.tsx`, `.mts`, and `.cts` modules
10
11
  - **React Integration** - Import React hooks and components via moduleResolver
11
12
  - **CSP-Safe** - Tree-walking interpreter, no eval() or new Function()
12
13
  - **ASI (Automatic Semicolon Insertion)** - Write JavaScript naturally without mandatory semicolons
@@ -143,9 +144,10 @@ const moduleResolver = {
143
144
 
144
145
  ```javascript
145
146
  const moduleResolver = {
146
- async resolve(modulePath) {
147
+ async resolve(modulePath, fromPath) {
147
148
  if (modulePath === './utils') {
148
149
  return {
150
+ path: '/virtual/project/utils.js',
149
151
  code: `
150
152
  export function double(x) { return x * 2; }
151
153
  export const PI = 3.14159;
@@ -157,6 +159,40 @@ const moduleResolver = {
157
159
  };
158
160
  ```
159
161
 
162
+ `fromPath` is the path of the importing module. For top-level code, pass `sourcePath` to `execute()` so relative imports have a deterministic root. For nested imports, JSLike passes the resolved module `path` returned by the resolver.
163
+
164
+ ```javascript
165
+ const files = {
166
+ '/virtual/project/main.ts': `
167
+ import { user } from './user.ts';
168
+ user.name
169
+ `,
170
+ '/virtual/project/user.ts': `
171
+ type User = { name: string };
172
+ export const user: User = { name: 'Ada' };
173
+ `
174
+ };
175
+
176
+ const moduleResolver = {
177
+ async resolve(modulePath, fromPath) {
178
+ const base = new URL('.', `file://${fromPath}`).pathname;
179
+ const resolvedPath = new URL(modulePath, `file://${base}`).pathname;
180
+
181
+ if (!files[resolvedPath]) return null;
182
+ return {
183
+ path: resolvedPath,
184
+ code: files[resolvedPath]
185
+ };
186
+ }
187
+ };
188
+
189
+ const result = await execute(files['/virtual/project/main.ts'], null, {
190
+ moduleResolver,
191
+ sourcePath: '/virtual/project/main.ts'
192
+ });
193
+ // "Ada"
194
+ ```
195
+
160
196
  ### Import Styles Supported
161
197
 
162
198
  ```javascript
@@ -165,6 +201,44 @@ import React from 'react'; // Default import
165
201
  import * as Utils from './utils'; // Namespace import
166
202
  ```
167
203
 
204
+ ## TypeScript Support
205
+
206
+ JSLike parses TypeScript and TSX with a bundled `@sveltejs/acorn-typescript` parser. TypeScript syntax is enabled automatically for `sourcePath` and resolved module paths ending in `.ts`, `.tsx`, `.mts`, or `.cts`. You can also force it with `typescript: true` or `tsx: true`.
207
+
208
+ ```javascript
209
+ const result = await execute(`
210
+ interface User {
211
+ name: string;
212
+ }
213
+
214
+ enum Role {
215
+ Admin,
216
+ Member
217
+ }
218
+
219
+ class Account {
220
+ constructor(public user: User, readonly role: Role) {}
221
+ }
222
+
223
+ const account = new Account({ name: 'Ada' }, Role.Admin);
224
+ account.user.name + ':' + account.role
225
+ `, null, {
226
+ sourcePath: '/virtual/account.ts'
227
+ });
228
+ // "Ada:0"
229
+ ```
230
+
231
+ Supported TypeScript runtime behavior:
232
+
233
+ - Type-only declarations and annotations are erased: `type`, `interface`, `declare`, parameter/return annotations, tuple/readonly annotations.
234
+ - Type-only imports and exports do not trigger runtime module resolution.
235
+ - Type wrappers evaluate their inner expression: `as`, `<T>value`, `satisfies`, non-null `!`, generic call instantiation.
236
+ - Enums execute as runtime enum objects, including numeric reverse mappings and string enum members.
237
+ - Constructor parameter properties assign to `this`, including `public`, `private`, and `readonly`.
238
+ - TSX parses and executes JSX in `.tsx` files or with `tsx: true`.
239
+
240
+ Unsupported runtime TypeScript constructs throw explicit errors instead of executing incorrectly. This currently includes `namespace`, `export =`, and `import x = require(...)`.
241
+
168
242
  ## JSX Features
169
243
 
170
244
  ### Basic Elements
@@ -54,22 +54,48 @@ function getAvailableMethods(obj) {
54
54
 
55
55
  const methods = new Set();
56
56
 
57
- // Get own properties
58
- Object.getOwnPropertyNames(obj).forEach(name => {
59
- if (typeof obj[name] === 'function') {
60
- methods.add(name);
57
+ const addSafeMethods = (target, includeConstructor = true) => {
58
+ let names;
59
+ try {
60
+ names = Object.getOwnPropertyNames(target);
61
+ } catch {
62
+ return;
61
63
  }
62
- });
63
64
 
64
- // Get prototype methods
65
- let proto = Object.getPrototypeOf(obj);
66
- while (proto && proto !== Object.prototype) {
67
- Object.getOwnPropertyNames(proto).forEach(name => {
68
- if (typeof proto[name] === 'function' && name !== 'constructor') {
65
+ names.forEach(name => {
66
+ if (!includeConstructor && name === 'constructor') return;
67
+
68
+ let descriptor;
69
+ try {
70
+ descriptor = Object.getOwnPropertyDescriptor(target, name);
71
+ } catch {
72
+ return;
73
+ }
74
+
75
+ if (descriptor && 'value' in descriptor && typeof descriptor.value === 'function') {
69
76
  methods.add(name);
70
77
  }
71
78
  });
72
- proto = Object.getPrototypeOf(proto);
79
+ };
80
+
81
+ // Get own properties
82
+ addSafeMethods(obj);
83
+
84
+ // Get prototype methods
85
+ let proto;
86
+ try {
87
+ proto = Object.getPrototypeOf(obj);
88
+ } catch {
89
+ proto = null;
90
+ }
91
+
92
+ while (proto && proto !== Object.prototype) {
93
+ addSafeMethods(proto, false);
94
+ try {
95
+ proto = Object.getPrototypeOf(proto);
96
+ } catch {
97
+ proto = null;
98
+ }
73
99
  }
74
100
 
75
101
  return Array.from(methods).sort();
package/dist/esm/index.js CHANGED
@@ -130,7 +130,8 @@ export async function execute(code, env = null, options = {}) {
130
130
  moduleResolver: options.moduleResolver,
131
131
  abortSignal: options.abortSignal,
132
132
  executionController: controller,
133
- currentModulePath: options.sourcePath
133
+ currentModulePath: options.sourcePath,
134
+ isTypeScriptModule: options.typescript || options.tsx || isTypeScriptPath(options.sourcePath)
134
135
  });
135
136
 
136
137
  // Use async evaluation if:
@@ -57,6 +57,174 @@ function getPatternName(pattern) {
57
57
  return pattern.name;
58
58
  }
59
59
 
60
+ function collectRuntimeIdentifierReferences(node) {
61
+ const references = new Set();
62
+ const skipKeys = new Set([
63
+ 'type',
64
+ 'start',
65
+ 'end',
66
+ 'loc',
67
+ 'range',
68
+ 'raw',
69
+ 'typeAnnotation',
70
+ 'returnType',
71
+ 'typeParameters',
72
+ 'typeArguments',
73
+ 'implements'
74
+ ]);
75
+
76
+ const visitPatternDefaults = (pattern) => {
77
+ if (!pattern || typeof pattern !== 'object') return;
78
+ if (pattern.type === 'AssignmentPattern') {
79
+ visit(pattern.right);
80
+ visitPatternDefaults(pattern.left);
81
+ } else if (pattern.type === 'ObjectPattern') {
82
+ for (const property of pattern.properties || []) {
83
+ visitPatternDefaults(property.value || property.argument);
84
+ }
85
+ } else if (pattern.type === 'ArrayPattern') {
86
+ for (const element of pattern.elements || []) {
87
+ visitPatternDefaults(element);
88
+ }
89
+ } else if (pattern.type === 'RestElement') {
90
+ visitPatternDefaults(pattern.argument);
91
+ } else if (pattern.type === 'TSParameterProperty') {
92
+ visitPatternDefaults(pattern.parameter);
93
+ }
94
+ };
95
+
96
+ const visitFunction = (fn) => {
97
+ for (const param of fn.params || []) {
98
+ visitPatternDefaults(param);
99
+ }
100
+ visit(fn.body);
101
+ };
102
+
103
+ const visitJSXName = (jsxName) => {
104
+ if (!jsxName || typeof jsxName !== 'object') return;
105
+ if (jsxName.type === 'JSXIdentifier') {
106
+ if (/^[A-Z]/.test(jsxName.name)) {
107
+ references.add(jsxName.name);
108
+ }
109
+ } else if (jsxName.type === 'JSXMemberExpression') {
110
+ visitJSXName(jsxName.object);
111
+ } else if (jsxName.type === 'JSXNamespacedName') {
112
+ visitJSXName(jsxName.namespace);
113
+ }
114
+ };
115
+
116
+ const visit = (current, parent = null, parentKey = null) => {
117
+ if (!current || typeof current !== 'object') return;
118
+
119
+ if (Array.isArray(current)) {
120
+ for (const item of current) visit(item, parent, parentKey);
121
+ return;
122
+ }
123
+
124
+ if (current.type?.startsWith('TS')) {
125
+ if (isTypeWrapperExpression(current)) {
126
+ visit(getTypeWrapperInnerExpression(current), current, 'expression');
127
+ } else if (current.type === 'TSEnumDeclaration') {
128
+ for (const member of current.members || []) {
129
+ visit(member.initializer);
130
+ }
131
+ }
132
+ return;
133
+ }
134
+
135
+ switch (current.type) {
136
+ case 'Identifier':
137
+ references.add(current.name);
138
+ return;
139
+ case 'ImportDeclaration':
140
+ return;
141
+ case 'ExportNamedDeclaration':
142
+ if (current.declaration) {
143
+ visit(current.declaration, current, 'declaration');
144
+ } else if (current.exportKind !== 'type') {
145
+ for (const specifier of current.specifiers || []) {
146
+ if (specifier.exportKind !== 'type' && specifier.local?.name) {
147
+ references.add(specifier.local.name);
148
+ }
149
+ }
150
+ }
151
+ return;
152
+ case 'ExportDefaultDeclaration':
153
+ visit(current.declaration, current, 'declaration');
154
+ return;
155
+ case 'VariableDeclarator':
156
+ visitPatternDefaults(current.id);
157
+ visit(current.init, current, 'init');
158
+ return;
159
+ case 'FunctionDeclaration':
160
+ visitFunction(current);
161
+ return;
162
+ case 'FunctionExpression':
163
+ case 'ArrowFunctionExpression':
164
+ visitFunction(current);
165
+ return;
166
+ case 'ClassDeclaration':
167
+ case 'ClassExpression':
168
+ visit(current.superClass, current, 'superClass');
169
+ visit(current.body, current, 'body');
170
+ return;
171
+ case 'MemberExpression':
172
+ case 'OptionalMemberExpression':
173
+ visit(current.object, current, 'object');
174
+ if (current.computed) {
175
+ visit(current.property, current, 'property');
176
+ }
177
+ return;
178
+ case 'Property':
179
+ if (current.computed) {
180
+ visit(current.key, current, 'key');
181
+ }
182
+ visit(current.value, current, 'value');
183
+ return;
184
+ case 'MethodDefinition':
185
+ case 'PropertyDefinition':
186
+ if (current.computed) {
187
+ visit(current.key, current, 'key');
188
+ }
189
+ if (!current.declare && !current.abstract) {
190
+ visit(current.value, current, 'value');
191
+ }
192
+ return;
193
+ case 'AssignmentPattern':
194
+ visit(current.right, current, 'right');
195
+ return;
196
+ case 'RestElement':
197
+ return;
198
+ case 'ObjectPattern':
199
+ case 'ArrayPattern':
200
+ visitPatternDefaults(current);
201
+ return;
202
+ case 'JSXElement':
203
+ visitJSXName(current.openingElement?.name);
204
+ for (const child of current.children || []) {
205
+ visit(child);
206
+ }
207
+ return;
208
+ case 'JSXFragment':
209
+ for (const child of current.children || []) {
210
+ visit(child);
211
+ }
212
+ return;
213
+ case 'JSXExpressionContainer':
214
+ visit(current.expression, current, 'expression');
215
+ return;
216
+ }
217
+
218
+ for (const [key, value] of Object.entries(current)) {
219
+ if (skipKeys.has(key)) continue;
220
+ visit(value, current, key);
221
+ }
222
+ };
223
+
224
+ visit(node);
225
+ return references;
226
+ }
227
+
60
228
  export class Interpreter {
61
229
  constructor(globalEnv, options = {}) {
62
230
  this.globalEnv = globalEnv;
@@ -65,6 +233,8 @@ export class Interpreter {
65
233
  this.moduleResolutionCache = options.moduleResolutionCache || new Map();
66
234
  this.moduleExports = {}; // Track exports in current module
67
235
  this.currentModulePath = options.currentModulePath;
236
+ this.isTypeScriptModule = options.isTypeScriptModule || false;
237
+ this.runtimeIdentifierReferences = null;
68
238
  this.abortSignal = options.abortSignal;
69
239
  this.executionController = options.executionController;
70
240
  }
@@ -287,15 +457,21 @@ export class Interpreter {
287
457
 
288
458
  // For Program nodes (evaluate all statements async)
289
459
  if (node.type === 'Program') {
460
+ const previousReferences = this.runtimeIdentifierReferences;
461
+ this.runtimeIdentifierReferences = collectRuntimeIdentifierReferences(node);
290
462
  let result = undefined;
291
- for (const statement of node.body) {
292
- result = await this.evaluateAsync(statement, env);
293
- // Handle top-level return and throw
294
- if (result instanceof ReturnValue || result instanceof ThrowSignal) {
295
- return result;
463
+ try {
464
+ for (const statement of node.body) {
465
+ result = await this.evaluateAsync(statement, env);
466
+ // Handle top-level return and throw
467
+ if (result instanceof ReturnValue || result instanceof ThrowSignal) {
468
+ return result;
469
+ }
296
470
  }
471
+ return result;
472
+ } finally {
473
+ this.runtimeIdentifierReferences = previousReferences;
297
474
  }
298
- return result;
299
475
  }
300
476
 
301
477
  // For import declarations (always async)
@@ -1098,27 +1274,33 @@ export class Interpreter {
1098
1274
  }
1099
1275
 
1100
1276
  evaluateProgram(node, env) {
1277
+ const previousReferences = this.runtimeIdentifierReferences;
1278
+ this.runtimeIdentifierReferences = collectRuntimeIdentifierReferences(node);
1101
1279
  let result = undefined;
1102
- for (let i = 0; i < node.body.length; i++) {
1103
- const statement = node.body[i];
1104
- const isLast = i === node.body.length - 1;
1105
-
1106
- // Special case: Last statement is a BlockStatement that looks like object literal
1107
- // Handle both shorthand { x, y } and full syntax { key: value, key2: value2 }
1108
- if (isLast && statement.type === 'BlockStatement') {
1109
- const objLiteral = this.tryConvertBlockToObjectLiteral(statement, env);
1110
- if (objLiteral !== null) {
1111
- return objLiteral;
1280
+ try {
1281
+ for (let i = 0; i < node.body.length; i++) {
1282
+ const statement = node.body[i];
1283
+ const isLast = i === node.body.length - 1;
1284
+
1285
+ // Special case: Last statement is a BlockStatement that looks like object literal
1286
+ // Handle both shorthand { x, y } and full syntax { key: value, key2: value2 }
1287
+ if (isLast && statement.type === 'BlockStatement') {
1288
+ const objLiteral = this.tryConvertBlockToObjectLiteral(statement, env);
1289
+ if (objLiteral !== null) {
1290
+ return objLiteral;
1291
+ }
1112
1292
  }
1113
- }
1114
1293
 
1115
- const statementResult = this.evaluate(statement, env);
1116
- if (statementResult instanceof ReturnValue || statementResult instanceof ThrowSignal) {
1117
- return statementResult;
1294
+ const statementResult = this.evaluate(statement, env);
1295
+ if (statementResult instanceof ReturnValue || statementResult instanceof ThrowSignal) {
1296
+ return statementResult;
1297
+ }
1298
+ result = statementResult;
1118
1299
  }
1119
- result = statementResult;
1300
+ return result;
1301
+ } finally {
1302
+ this.runtimeIdentifierReferences = previousReferences;
1120
1303
  }
1121
- return result;
1122
1304
  }
1123
1305
 
1124
1306
  // Try to convert a BlockStatement to an object literal
@@ -1960,6 +2142,12 @@ export class Interpreter {
1960
2142
  return undefined;
1961
2143
  }
1962
2144
 
2145
+ if (this.isTypeScriptModule &&
2146
+ node.specifiers.length > 0 &&
2147
+ node.specifiers.every(specifier => !this.isRuntimeImportSpecifier(specifier))) {
2148
+ return undefined;
2149
+ }
2150
+
1963
2151
  // Check if module resolver is configured
1964
2152
  if (!this.moduleResolver) {
1965
2153
  throw new Error('Module resolver not configured - cannot import modules');
@@ -2012,6 +2200,7 @@ export class Interpreter {
2012
2200
  moduleResolver: this.moduleResolver,
2013
2201
  moduleResolutionCache: this.moduleResolutionCache,
2014
2202
  currentModulePath: resolvedPath,
2203
+ isTypeScriptModule: isTypeScriptPath(resolvedPath),
2015
2204
  abortSignal: this.abortSignal,
2016
2205
  executionController: this.executionController
2017
2206
  });
@@ -2033,7 +2222,7 @@ export class Interpreter {
2033
2222
  bindImportSpecifiers(node, env, modulePath, moduleExports) {
2034
2223
  // Import specified bindings into current environment
2035
2224
  for (const specifier of node.specifiers) {
2036
- if (specifier.importKind === 'type') {
2225
+ if (!this.isRuntimeImportSpecifier(specifier)) {
2037
2226
  continue;
2038
2227
  }
2039
2228
 
@@ -2066,6 +2255,23 @@ export class Interpreter {
2066
2255
  return undefined;
2067
2256
  }
2068
2257
 
2258
+ isRuntimeImportSpecifier(specifier) {
2259
+ if (specifier.importKind === 'type') {
2260
+ return false;
2261
+ }
2262
+
2263
+ if (!this.isTypeScriptModule) {
2264
+ return true;
2265
+ }
2266
+
2267
+ const localName = specifier.local?.name;
2268
+ if (!localName || !this.runtimeIdentifierReferences) {
2269
+ return true;
2270
+ }
2271
+
2272
+ return this.runtimeIdentifierReferences.has(localName);
2273
+ }
2274
+
2069
2275
  evaluateExportNamedDeclaration(node, env) {
2070
2276
  if (node.exportKind === 'type' || isTypeOnlyDeclaration(node.declaration)) {
2071
2277
  return undefined;