rip-lang 2.7.1 → 2.7.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/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ All notable changes to Rip will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ ## [2.7.2] - 2026-02-03
11
+
12
+ ### Clean ES Module REPL
13
+
14
+ **Proper `vm.SourceTextModule` Implementation**: The REPL now uses Node's standard ES module API instead of temp files:
15
+
16
+ ```coffee
17
+ rip> { Cash } = await import("./utils.rip")
18
+ rip> config = Cash((await import("./config.rip")).default)
19
+ rip> config.app.name
20
+ → 'Rip Labs API'
21
+ ```
22
+
23
+ **Key improvements:**
24
+ - Uses `vm.SourceTextModule` for in-memory module evaluation (no temp files)
25
+ - `.rip` files compiled on-the-fly via module linker
26
+ - Cross-runtime compatible (Node.js, Bun, potentially Deno)
27
+ - Dynamic `await import()` transformed to static imports automatically
28
+ - Clean variable persistence through VM context
29
+
30
+ This is the standard way to handle ES modules in sandboxed contexts - no hacks required.
31
+
32
+ ---
33
+
10
34
  ## [2.7.1] - 2026-02-03
11
35
 
12
36
  ### Bun-Native REPL with Dynamic Import Support
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-2.7.1-blue.svg" alt="Version"></a>
12
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-2.7.2-blue.svg" alt="Version"></a>
13
13
  <a href="#zero-dependencies"><img src="https://img.shields.io/badge/dependencies-ZERO-brightgreen.svg" alt="Dependencies"></a>
14
14
  <a href="#"><img src="https://img.shields.io/badge/tests-979%2F979-brightgreen.svg" alt="Tests"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
package/bin/rip CHANGED
@@ -130,7 +130,12 @@ async function main() {
130
130
  // Launch REPL if: no args AND stdin is a TTY (not piped), OR explicit -r flag
131
131
  const isTTY = process.stdin.isTTY;
132
132
  if ((args.length === 0 && isTTY) || ripOptions.includes('-r') || ripOptions.includes('--repl')) {
133
- startREPL();
133
+ // Spawn REPL with --experimental-vm-modules flag for proper ES module support
134
+ const replModule = join(__dirname, '../src/repl.js');
135
+ const replProcess = spawn('bun', ['--experimental-vm-modules', '-e', `import('${replModule}').then(m => m.startREPL())`], {
136
+ stdio: 'inherit'
137
+ });
138
+ replProcess.on('exit', (code) => process.exit(code || 0));
134
139
  return;
135
140
  }
136
141
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  That "Why Not" document makes strong arguments, but here's the **counter-argument**—a working, tested, **production-ready** language that offers a different path.
8
8
 
9
- **Rip isn't vaporware. It's real. Version 2.7.1. 979/979 tests passing. Self-hosting. Zero dependencies. Available now.**
9
+ **Rip isn't vaporware. It's real. Version 2.7.2. 979/979 tests passing. Self-hosting. Zero dependencies. Available now.**
10
10
 
11
11
  ### The Philosophical Divide: Freedom vs Fear
12
12
 
@@ -706,7 +706,7 @@ Rip isn't about going backward. It's about recognizing that **we took a wrong tu
706
706
 
707
707
  **The future isn't more dependencies. It's zero dependencies.**
708
708
 
709
- **The future is Rip. Version 2.7.1. Available today.**
709
+ **The future is Rip. Version 2.7.2. Available today.**
710
710
 
711
711
  ---
712
712
 
@@ -752,6 +752,6 @@ $ echo 'console.log "Hello, Rip!"' > test.rip && bun test.rip
752
752
  - ✅ **Ruby constructors** (`ClassName.new()` - elegant instantiation)
753
753
  - ✅ **Framework-agnostic** (use with React, Vue, Svelte, or vanilla JS!)
754
754
 
755
- **Version 2.7.1. Available now. Clone and go.**
755
+ **Version 2.7.2. Available now. Clone and go.**
756
756
 
757
757
  This approach is ready. Give it a try.
@@ -7488,8 +7488,8 @@ function compileToJS(source, options = {}) {
7488
7488
  return new Compiler(options).compileToJS(source);
7489
7489
  }
7490
7490
  // src/browser.js
7491
- var VERSION = "2.7.1";
7492
- var BUILD_DATE = "2026-02-03@10:57:35GMT";
7491
+ var VERSION = "2.7.2";
7492
+ var BUILD_DATE = "2026-02-03@11:22:57GMT";
7493
7493
  var dedent = (s) => {
7494
7494
  const m = s.match(/^[ \t]*(?=\S)/gm);
7495
7495
  const i = Math.min(...(m || []).map((x) => x.length));
@@ -527,4 +527,4 @@ function __catchErrors(fn) {
527
527
  `),$=E.findIndex((F)=>F==="__DATA__");if($!==-1){let F=E.slice($+1);_=F.length>0?F.join(`
528
528
  `)+`
529
529
  `:"",U=E.slice(0,$).join(`
530
- `)}let R=new x1().tokenize(U);if(this.options.showTokens)R.forEach((F)=>console.log(`${F[0].padEnd(12)} ${JSON.stringify(F[1])}`)),console.log();_1.lexer={tokens:R,pos:0,setInput:function(){},lex:function(){if(this.pos>=this.tokens.length)return 1;let F=this.tokens[this.pos++];return this.yytext=F[1],this.yylloc=F[2],F[0]}};let X;try{X=_1.parse(U)}catch(F){if(/\?\s*\([^)]*\?[^)]*:[^)]*\)\s*:/.test(U)||/\?\s+\w+\s+\?\s+/.test(U))throw Error("Nested ternary operators are not supported. Use if/else statements instead.");throw F}if(this.options.showSExpr)console.log(a(X,0,!0)),console.log();let Y=new k({dataSection:_,skipReactiveRuntime:this.options.skipReactiveRuntime,reactiveVars:this.options.reactiveVars}),M=Y.compile(X);return{tokens:R,sexpr:X,code:M,data:_,reactiveVars:Y.reactiveVars}}compileToJS(U){return this.compile(U).code}compileToSExpr(U){return this.compile(U).sexpr}}function o2(U,_={}){return new H1(_).compile(U)}function N1(U,_={}){return new H1(_).compileToJS(U)}var Y3="2.7.1",M3="2026-02-03@10:57:35GMT",t2=(U)=>{let _=U.match(/^[ \t]*(?=\S)/gm),E=Math.min(...(_||[]).map(($)=>$.length));return U.replace(RegExp(`^[ ]{${E}}`,"gm"),"").trim()};async function v2(){let U=document.querySelectorAll('script[type="text/rip"]');for(let _ of U){if(_.hasAttribute("data-rip-processed"))continue;try{let E=t2(_.textContent),$=N1(E);(0,eval)($),_.setAttribute("data-rip-processed","true")}catch(E){console.error("Error compiling Rip script:",E),console.error("Script content:",_.textContent)}}}if(typeof document<"u")if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",v2);else v2();function e2(U){try{let E=N1(U).replace(/^let\s+[^;]+;\s*\n\s*/m,"");E=E.replace(/^const\s+/gm,"var ");let $=(0,eval)(E);if($!==void 0)globalThis._=$;return $}catch(_){console.error("Rip compilation error:",_.message);return}}if(typeof globalThis<"u")globalThis.rip=e2;export{e2 as rip,v2 as processRipScripts,_1 as parser,a as formatSExpr,N1 as compileToJS,o2 as compile,Y3 as VERSION,x1 as Lexer,H1 as Compiler,k as CodeGenerator,M3 as BUILD_DATE};
530
+ `)}let R=new x1().tokenize(U);if(this.options.showTokens)R.forEach((F)=>console.log(`${F[0].padEnd(12)} ${JSON.stringify(F[1])}`)),console.log();_1.lexer={tokens:R,pos:0,setInput:function(){},lex:function(){if(this.pos>=this.tokens.length)return 1;let F=this.tokens[this.pos++];return this.yytext=F[1],this.yylloc=F[2],F[0]}};let X;try{X=_1.parse(U)}catch(F){if(/\?\s*\([^)]*\?[^)]*:[^)]*\)\s*:/.test(U)||/\?\s+\w+\s+\?\s+/.test(U))throw Error("Nested ternary operators are not supported. Use if/else statements instead.");throw F}if(this.options.showSExpr)console.log(a(X,0,!0)),console.log();let Y=new k({dataSection:_,skipReactiveRuntime:this.options.skipReactiveRuntime,reactiveVars:this.options.reactiveVars}),M=Y.compile(X);return{tokens:R,sexpr:X,code:M,data:_,reactiveVars:Y.reactiveVars}}compileToJS(U){return this.compile(U).code}compileToSExpr(U){return this.compile(U).sexpr}}function o2(U,_={}){return new H1(_).compile(U)}function N1(U,_={}){return new H1(_).compileToJS(U)}var Y3="2.7.2",M3="2026-02-03@11:22:57GMT",t2=(U)=>{let _=U.match(/^[ \t]*(?=\S)/gm),E=Math.min(...(_||[]).map(($)=>$.length));return U.replace(RegExp(`^[ ]{${E}}`,"gm"),"").trim()};async function v2(){let U=document.querySelectorAll('script[type="text/rip"]');for(let _ of U){if(_.hasAttribute("data-rip-processed"))continue;try{let E=t2(_.textContent),$=N1(E);(0,eval)($),_.setAttribute("data-rip-processed","true")}catch(E){console.error("Error compiling Rip script:",E),console.error("Script content:",_.textContent)}}}if(typeof document<"u")if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",v2);else v2();function e2(U){try{let E=N1(U).replace(/^let\s+[^;]+;\s*\n\s*/m,"");E=E.replace(/^const\s+/gm,"var ");let $=(0,eval)(E);if($!==void 0)globalThis._=$;return $}catch(_){console.error("Rip compilation error:",_.message);return}}if(typeof globalThis<"u")globalThis.rip=e2;export{e2 as rip,v2 as processRipScripts,_1 as parser,a as formatSExpr,N1 as compileToJS,o2 as compile,Y3 as VERSION,x1 as Lexer,H1 as Compiler,k as CodeGenerator,M3 as BUILD_DATE};
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
package/src/repl.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Rip REPL - Interactive Read-Eval-Print Loop (Bun-native version)
2
+ * Rip REPL - Interactive Read-Eval-Print Loop
3
3
  *
4
4
  * Features:
5
5
  * - Multi-line input detection
@@ -7,7 +7,8 @@
7
7
  * - Special commands (.help, .clear, .vars, etc.)
8
8
  * - Colored output
9
9
  * - Persistent context
10
- * - Full dynamic import() support (Bun-native)
10
+ * - Native ES module support via vm.SourceTextModule
11
+ * - Direct .rip file imports (compiled on-the-fly)
11
12
  */
12
13
 
13
14
  import * as readline from 'readline';
@@ -15,8 +16,8 @@ import { inspect } from 'util';
15
16
  import * as fs from 'fs';
16
17
  import * as path from 'path';
17
18
  import * as os from 'os';
18
- import { pathToFileURL } from 'url';
19
- import { Compiler } from './compiler.js';
19
+ import * as vm from 'vm';
20
+ import { Compiler, compileToJS } from './compiler.js';
20
21
  import packageJson from '../package.json' with { type: 'json' };
21
22
 
22
23
  const VERSION = packageJson.version;
@@ -35,21 +36,37 @@ const colors = {
35
36
  gray: '\x1b[90m'
36
37
  };
37
38
 
38
- // Use globalThis for persistent context (works across async evaluations)
39
- globalThis.__ripRepl = globalThis.__ripRepl || {
40
- vars: {},
41
- tempCounter: 0,
42
- cwd: process.cwd()
43
- };
44
-
45
39
  export class RipREPL {
46
40
  constructor() {
47
41
  this.buffer = ''; // Multi-line input buffer
48
42
  this.history = []; // Command history
49
43
  this.historyFile = path.join(os.homedir(), '.rip_history');
50
44
  this.reactiveVars = new Set(); // Track reactive variables across lines
45
+ this.cwd = process.cwd();
46
+
47
+ // Persisted variables across evaluations
48
+ this.vars = {};
49
+
50
+ // Module cache for linked modules
51
+ this.moduleCache = new Map();
52
+
53
+ // VM context with necessary globals
54
+ this.vmContext = vm.createContext({
55
+ console,
56
+ process,
57
+ setTimeout,
58
+ setInterval,
59
+ clearTimeout,
60
+ clearInterval,
61
+ Buffer,
62
+ URL,
63
+ URLSearchParams,
64
+ TextEncoder,
65
+ TextDecoder,
66
+ __vars: this.vars // Reference to persisted variables
67
+ });
51
68
 
52
- // Inject reactive runtime into global context
69
+ // Inject reactive runtime
53
70
  this.injectReactiveRuntime();
54
71
 
55
72
  this.rl = readline.createInterface({
@@ -65,11 +82,7 @@ export class RipREPL {
65
82
  }
66
83
 
67
84
  get context() {
68
- return globalThis.__ripRepl.vars;
69
- }
70
-
71
- set context(val) {
72
- globalThis.__ripRepl.vars = val;
85
+ return this.vars;
73
86
  }
74
87
 
75
88
  getPrompt() {
@@ -87,7 +100,6 @@ export class RipREPL {
87
100
  });
88
101
 
89
102
  this.rl.on('close', () => {
90
- this.cleanup();
91
103
  this.saveHistory();
92
104
  console.log(`\n${colors.gray}Goodbye!${colors.reset}`);
93
105
  process.exit(0);
@@ -97,30 +109,29 @@ export class RipREPL {
97
109
  }
98
110
 
99
111
  printWelcome() {
100
- console.log(`${colors.bright}Rip ${VERSION}${colors.reset} - Interactive REPL ${colors.dim}(Bun-native)${colors.reset}`);
112
+ console.log(`${colors.bright}Rip ${VERSION}${colors.reset} - Interactive REPL`);
101
113
  console.log(`${colors.gray}Type ${colors.cyan}.help${colors.gray} for commands, ${colors.cyan}Ctrl+C${colors.gray} to exit${colors.reset}\n`);
102
114
  }
103
115
 
104
116
  injectReactiveRuntime() {
105
- // Inject reactive runtime into global context once
106
- if (globalThis.__ripRepl.runtimeInjected) return;
107
-
108
- // Define reactive primitives on globalThis so they're available in all evaluations
109
- globalThis.__currentEffect = null;
110
- globalThis.__pendingEffects = new Set();
117
+ // Define reactive primitives in the VM context
118
+ const ctx = this.vmContext;
119
+
120
+ ctx.__currentEffect = null;
121
+ ctx.__pendingEffects = new Set();
111
122
 
112
- globalThis.__state = function(v) {
123
+ ctx.__state = function(v) {
113
124
  const subs = new Set();
114
125
  let notifying = false, locked = false, dead = false;
115
126
  const s = {
116
- get value() { if (dead) return v; if (__currentEffect) { subs.add(__currentEffect); __currentEffect.dependencies.add(subs); } return v; },
127
+ get value() { if (dead) return v; if (ctx.__currentEffect) { subs.add(ctx.__currentEffect); ctx.__currentEffect.dependencies.add(subs); } return v; },
117
128
  set value(n) {
118
129
  if (dead || locked || n === v || notifying) return;
119
130
  v = n;
120
131
  notifying = true;
121
132
  for (const sub of subs) if (sub.markDirty) sub.markDirty();
122
- for (const sub of subs) if (!sub.markDirty) __pendingEffects.add(sub);
123
- const fx = [...__pendingEffects]; __pendingEffects.clear();
133
+ for (const sub of subs) if (!sub.markDirty) ctx.__pendingEffects.add(sub);
134
+ const fx = [...ctx.__pendingEffects]; ctx.__pendingEffects.clear();
124
135
  for (const e of fx) e.run();
125
136
  notifying = false;
126
137
  },
@@ -135,21 +146,21 @@ export class RipREPL {
135
146
  return s;
136
147
  };
137
148
 
138
- globalThis.__computed = function(fn) {
149
+ ctx.__computed = function(fn) {
139
150
  let v, dirty = true, locked = false, dead = false;
140
151
  const subs = new Set();
141
152
  const c = {
142
153
  dependencies: new Set(),
143
154
  markDirty() {
144
- if (dead || locked || !dirty) { if (!dead && !locked && !dirty) { dirty = true; for (const s of subs) if (s.markDirty) s.markDirty(); for (const s of subs) if (!s.markDirty) __pendingEffects.add(s); } }
155
+ if (dead || locked || !dirty) { if (!dead && !locked && !dirty) { dirty = true; for (const s of subs) if (s.markDirty) s.markDirty(); for (const s of subs) if (!s.markDirty) ctx.__pendingEffects.add(s); } }
145
156
  },
146
157
  get value() {
147
158
  if (dead) return v;
148
- if (__currentEffect) { subs.add(__currentEffect); __currentEffect.dependencies.add(subs); }
159
+ if (ctx.__currentEffect) { subs.add(ctx.__currentEffect); ctx.__currentEffect.dependencies.add(subs); }
149
160
  if (dirty && !locked) {
150
161
  for (const d of c.dependencies) d.delete(c); c.dependencies.clear();
151
- const prev = __currentEffect; __currentEffect = c;
152
- try { v = fn(); } finally { __currentEffect = prev; }
162
+ const prev = ctx.__currentEffect; ctx.__currentEffect = c;
163
+ try { v = fn(); } finally { ctx.__currentEffect = prev; }
153
164
  dirty = false;
154
165
  }
155
166
  return v;
@@ -165,13 +176,13 @@ export class RipREPL {
165
176
  return c;
166
177
  };
167
178
 
168
- globalThis.__effect = function(fn) {
179
+ ctx.__effect = function(fn) {
169
180
  const e = {
170
181
  dependencies: new Set(),
171
182
  run() {
172
183
  for (const d of e.dependencies) d.delete(e); e.dependencies.clear();
173
- const prev = __currentEffect; __currentEffect = e;
174
- try { fn(); } finally { __currentEffect = prev; }
184
+ const prev = ctx.__currentEffect; ctx.__currentEffect = e;
185
+ try { fn(); } finally { ctx.__currentEffect = prev; }
175
186
  },
176
187
  free() { for (const d of e.dependencies) d.delete(e); e.dependencies.clear(); }
177
188
  };
@@ -179,10 +190,8 @@ export class RipREPL {
179
190
  return () => e.free();
180
191
  };
181
192
 
182
- globalThis.__batch = function(fn) { fn(); };
183
- globalThis.__readonly = function(v) { return Object.freeze({ value: v }); };
184
-
185
- globalThis.__ripRepl.runtimeInjected = true;
193
+ ctx.__batch = function(fn) { fn(); };
194
+ ctx.__readonly = function(v) { return Object.freeze({ value: v }); };
186
195
  }
187
196
 
188
197
  async handleLine(line) {
@@ -212,12 +221,8 @@ export class RipREPL {
212
221
  }
213
222
 
214
223
  isComplete(code) {
215
- // Simple heuristic: check for balanced braces/parens/brackets
216
- // and whether it ends with incomplete syntax
217
-
218
224
  if (!code.trim()) return true;
219
225
 
220
- // Count brackets
221
226
  let parens = 0, braces = 0, brackets = 0;
222
227
  let inString = false;
223
228
  let stringChar = null;
@@ -226,7 +231,6 @@ export class RipREPL {
226
231
  const char = code[i];
227
232
  const prev = i > 0 ? code[i - 1] : null;
228
233
 
229
- // Handle strings (skip counting inside strings)
230
234
  if ((char === '"' || char === "'") && prev !== '\\') {
231
235
  if (inString && char === stringChar) {
232
236
  inString = false;
@@ -239,7 +243,6 @@ export class RipREPL {
239
243
 
240
244
  if (inString) continue;
241
245
 
242
- // Count brackets
243
246
  if (char === '(') parens++;
244
247
  if (char === ')') parens--;
245
248
  if (char === '{') braces++;
@@ -248,64 +251,32 @@ export class RipREPL {
248
251
  if (char === ']') brackets--;
249
252
  }
250
253
 
251
- // Check if incomplete
252
- if (parens > 0 || braces > 0 || brackets > 0) {
253
- return false; // Has unclosed brackets
254
- }
254
+ if (parens > 0 || braces > 0 || brackets > 0) return false;
255
255
 
256
- // Check for trailing operators that suggest continuation
257
256
  const trimmed = code.trim();
258
257
 
259
- // Complete statements (don't wait for more input)
260
- if (trimmed.endsWith('++') || trimmed.endsWith('--')) {
261
- return true; // x++ and x-- are complete
262
- }
263
-
264
- // Check if ends with regex literal /pattern/flags
265
- // Regex can end with / followed by optional flags (gimsuvy)
266
- if (/\/[gimsuvy]*$/.test(trimmed)) {
267
- return true; // Likely a regex, complete
268
- }
269
-
270
- // Incomplete operators (wait for more input)
271
- if (trimmed.endsWith('\\') || trimmed.endsWith(',')) {
272
- return false;
273
- }
258
+ if (trimmed.endsWith('++') || trimmed.endsWith('--')) return true;
259
+ if (/\/[gimsuvy]*$/.test(trimmed)) return true;
260
+ if (trimmed.endsWith('\\') || trimmed.endsWith(',')) return false;
261
+ if (trimmed.endsWith('->') || trimmed.endsWith('=>')) return false;
274
262
 
275
- if (trimmed.endsWith('->') || trimmed.endsWith('=>')) {
276
- return false; // Arrow functions need body
277
- }
278
-
279
- // Assignment operators
280
263
  if (trimmed.endsWith('=') && !trimmed.endsWith('==') && !trimmed.endsWith('!=') &&
281
264
  !trimmed.endsWith('>=') && !trimmed.endsWith('<=') && !trimmed.endsWith('??=') &&
282
265
  !trimmed.endsWith('&&=') && !trimmed.endsWith('||=') && !trimmed.endsWith('=~')) {
283
266
  return false;
284
267
  }
285
268
 
286
- // Arithmetic operators (check AFTER ++ and -- are handled)
287
- if (trimmed.endsWith('+') || trimmed.endsWith('-')) {
288
- return false; // Binary + or -
289
- }
290
-
291
- if (trimmed.endsWith('*') && !trimmed.endsWith('**')) {
292
- return false;
293
- }
294
-
295
- // Division operator (check AFTER regex pattern)
296
- if (trimmed.endsWith('/') && !trimmed.endsWith('//') && !/\/[gimsuvy]*$/.test(trimmed)) {
297
- return false;
298
- }
269
+ if (trimmed.endsWith('+') || trimmed.endsWith('-')) return false;
270
+ if (trimmed.endsWith('*') && !trimmed.endsWith('**')) return false;
271
+ if (trimmed.endsWith('/') && !trimmed.endsWith('//') && !/\/[gimsuvy]*$/.test(trimmed)) return false;
299
272
 
300
273
  return true;
301
274
  }
302
275
 
303
276
  async evaluate(code) {
304
277
  try {
305
- // Add to history
306
278
  this.history.push(code);
307
279
 
308
- // Compile Rip to JavaScript
309
280
  const compiler = new Compiler({
310
281
  showTokens: this.showTokens,
311
282
  showSExpr: this.showSExp,
@@ -314,7 +285,6 @@ export class RipREPL {
314
285
  });
315
286
  const result = compiler.compile(code);
316
287
 
317
- // Capture any new reactive variables declared in this line
318
288
  if (result.reactiveVars) {
319
289
  for (const v of result.reactiveVars) {
320
290
  this.reactiveVars.add(v);
@@ -323,18 +293,15 @@ export class RipREPL {
323
293
 
324
294
  let js = result.code;
325
295
 
326
- // Show compiled JS if enabled
327
296
  if (this.showJS) {
328
297
  console.log(`${colors.gray}// Compiled JavaScript:${colors.reset}`);
329
298
  console.log(`${colors.dim}${js}${colors.reset}\n`);
330
299
  }
331
300
 
332
- // Execute using Bun's native async evaluation with import support
333
- const evalResult = await this.bunEval(js);
301
+ const evalResult = await this.moduleEval(js);
334
302
 
335
- // Store result in _ for convenience
336
303
  if (evalResult !== undefined) {
337
- this.context._ = evalResult;
304
+ this.vars._ = evalResult;
338
305
  this.printResult(evalResult);
339
306
  }
340
307
  } catch (error) {
@@ -342,94 +309,134 @@ export class RipREPL {
342
309
  }
343
310
  }
344
311
 
345
- async bunEval(js) {
346
- // Extract variable declarations from the compiled JS
347
- const varMatches = [...js.matchAll(/^let\s+(\w+)(?:,\s*(\w+))*;$/gm)];
348
- const declaredVars = new Set();
349
- for (const match of varMatches) {
350
- for (let i = 1; i < match.length; i++) {
351
- if (match[i]) declaredVars.add(match[i]);
352
- }
312
+ // Module linker for resolving imports
313
+ async linker(specifier, referencingModule) {
314
+ return this.resolveModule(specifier, referencingModule.identifier);
315
+ }
316
+
317
+ // Resolve and load a module
318
+ async resolveModule(specifier, referrer) {
319
+ // Resolve relative paths based on referrer location
320
+ let resolvedPath = specifier;
321
+ if (specifier.startsWith('./') || specifier.startsWith('../')) {
322
+ const referrerDir = referrer ? path.dirname(referrer) : this.cwd;
323
+ resolvedPath = path.resolve(referrerDir, specifier);
324
+ }
325
+
326
+ // Check cache
327
+ if (this.moduleCache.has(resolvedPath)) {
328
+ return this.moduleCache.get(resolvedPath);
329
+ }
330
+
331
+ // Handle .rip files - compile on the fly
332
+ if (resolvedPath.endsWith('.rip')) {
333
+ const source = fs.readFileSync(resolvedPath, 'utf-8');
334
+ const js = compileToJS(source);
335
+
336
+ const ripMod = new vm.SourceTextModule(js, {
337
+ context: this.vmContext,
338
+ identifier: resolvedPath
339
+ });
340
+ await ripMod.link(this.linker.bind(this));
341
+ await ripMod.evaluate();
342
+ this.moduleCache.set(resolvedPath, ripMod);
343
+ return ripMod;
353
344
  }
354
345
 
355
- // Also check for let x = ... pattern
356
- const assignMatches = [...js.matchAll(/^let\s+(\w+)\s*=/gm)];
357
- for (const match of assignMatches) {
346
+ // Import native/npm modules
347
+ const imported = await import(specifier);
348
+ const exportNames = [...new Set([...Object.keys(imported), 'default'])];
349
+
350
+ const synth = new vm.SyntheticModule(
351
+ exportNames,
352
+ function() {
353
+ for (const key of exportNames) {
354
+ if (key in imported) this.setExport(key, imported[key]);
355
+ }
356
+ },
357
+ { context: this.vmContext, identifier: specifier }
358
+ );
359
+
360
+ // SyntheticModule needs to be linked and evaluated
361
+ await synth.link(() => {});
362
+ await synth.evaluate();
363
+
364
+ this.moduleCache.set(resolvedPath, synth);
365
+ return synth;
366
+ }
367
+
368
+ // Evaluate using vm.SourceTextModule (no temp files!)
369
+ async moduleEval(js) {
370
+ // Extract declared variables
371
+ const declaredVars = new Set();
372
+ for (const match of js.matchAll(/^let\s+(\w+)/gm)) {
358
373
  declaredVars.add(match[1]);
359
374
  }
360
375
 
361
- // Build code that restores context
362
- const ctx = this.context;
363
- const existingVars = Object.keys(ctx);
376
+ // Transform await import() to static imports (workaround for Bun bug #24217)
377
+ const dynamicImports = [];
378
+ let counter = 0;
379
+ js = js.replace(/await\s+import\s*\(\s*(['"])([^'"]+)\1\s*\)/g, (match, quote, specifier) => {
380
+ const varName = '__import_' + counter++ + '__';
381
+ dynamicImports.push({ varName, specifier });
382
+ return varName;
383
+ });
384
+
385
+ const staticImports = dynamicImports
386
+ .map(({ varName, specifier }) => `import * as ${varName} from '${specifier}';`)
387
+ .join('\n');
364
388
 
365
- // Remove let declarations for vars we're restoring from context
366
- let cleanJs = js;
389
+ // Restore existing variables and remove duplicate declarations
390
+ const existingVars = Object.keys(this.vars);
367
391
  for (const v of existingVars) {
368
- cleanJs = cleanJs.replace(new RegExp(`^let ${v};\\n`, 'm'), '');
369
- cleanJs = cleanJs.replace(new RegExp(`^let ${v}(\\s*=)`, 'm'), `${v}$1`);
392
+ js = js.replace(new RegExp(`^let ${v};\\n?`, 'm'), '');
393
+ js = js.replace(new RegExp(`^let ${v}(\\s*=)`, 'm'), `${v}$1`);
370
394
  }
371
395
 
372
- // Restore existing variables
373
- const restoreCtx = existingVars
396
+ // Build restore code (get vars from __vars)
397
+ const restoreCode = existingVars
374
398
  .filter(k => k !== '_')
375
- .map(k => `let ${k} = globalThis.__ripRepl.vars["${k}"];`)
399
+ .map(v => `let ${v} = __vars['${v}'];`)
376
400
  .join('\n');
377
401
 
378
- // Find the last expression to capture as result
379
- const lines = cleanJs.trim().split('\n');
402
+ // Build save code (save vars back to __vars)
403
+ const allVars = [...new Set([...existingVars, ...declaredVars])].filter(k => k !== '_');
404
+ const saveCode = allVars
405
+ .map(v => `if (typeof ${v} !== 'undefined') __vars['${v}'] = ${v};`)
406
+ .join('\n');
407
+
408
+ // Extract last expression for result capture
409
+ const lines = js.trim().split('\n');
380
410
  let lastLine = lines[lines.length - 1];
381
411
 
382
- // If it's a simple expression (ends with ;), capture it
383
- if (lastLine && lastLine.endsWith(';') &&
384
- !lastLine.startsWith('let ') &&
385
- !lastLine.startsWith('const ') &&
386
- !lastLine.startsWith('var ') &&
387
- !lastLine.includes('import ') &&
388
- !lastLine.includes('export ')) {
389
- lines[lines.length - 1] = `globalThis.__ripRepl_result = ${lastLine.slice(0, -1)};`;
412
+ if (lastLine && !lastLine.startsWith('import ') && !lastLine.startsWith('export ') &&
413
+ !lastLine.startsWith('let ') && !lastLine.startsWith('const ')) {
414
+ if (lastLine.endsWith(';')) lastLine = lastLine.slice(0, -1);
415
+ lines[lines.length - 1] = '__result = ' + lastLine + ';';
390
416
  }
391
417
 
392
- // Save variables back to context
393
- const allVars = [...new Set([...existingVars, ...declaredVars])].filter(k => k !== '_');
394
- const saveCtx = allVars.map(v =>
395
- `if (typeof ${v} !== 'undefined') globalThis.__ripRepl.vars["${v}"] = ${v};`
396
- ).join('\n');
397
-
398
- const wrapped = `
399
- ${restoreCtx}
418
+ // Build module code
419
+ const moduleCode = `${staticImports}
420
+ ${restoreCode}
421
+ let __result;
400
422
  ${lines.join('\n')}
401
- ${saveCtx}
423
+ ${saveCode}
424
+ export { __result };
402
425
  `;
403
426
 
404
- // Write to temp file and import it (enables dynamic imports)
405
- const tempFile = path.join(globalThis.__ripRepl.cwd, `.rip-repl-${globalThis.__ripRepl.tempCounter++}.mjs`);
427
+ // Create and evaluate module
428
+ const mod = new vm.SourceTextModule(moduleCode, {
429
+ context: this.vmContext,
430
+ identifier: this.cwd + '/repl-' + Date.now()
431
+ });
406
432
 
407
- try {
408
- fs.writeFileSync(tempFile, wrapped);
409
- await import(pathToFileURL(tempFile).href + `?t=${Date.now()}`);
410
- const result = globalThis.__ripRepl_result;
411
- delete globalThis.__ripRepl_result;
412
- return result;
413
- } finally {
414
- try { fs.unlinkSync(tempFile); } catch {}
415
- }
416
- }
433
+ await mod.link(this.linker.bind(this));
434
+ await mod.evaluate();
417
435
 
418
- cleanup() {
419
- // Clean up any leftover temp files
420
- const cwd = globalThis.__ripRepl.cwd;
421
- try {
422
- const files = fs.readdirSync(cwd);
423
- for (const f of files) {
424
- if (f.startsWith('.rip-repl-') && f.endsWith('.mjs')) {
425
- fs.unlinkSync(path.join(cwd, f));
426
- }
427
- }
428
- } catch {}
436
+ return mod.namespace.__result;
429
437
  }
430
438
 
431
439
  printResult(value) {
432
- // Pretty print the result
433
440
  const formatted = inspect(value, {
434
441
  colors: true,
435
442
  depth: 3,
@@ -455,8 +462,9 @@ ${saveCtx}
455
462
  break;
456
463
 
457
464
  case '.clear':
458
- // Clear all variables
459
- globalThis.__ripRepl.vars = {};
465
+ this.vars = {};
466
+ this.vmContext.__vars = this.vars;
467
+ this.moduleCache.clear();
460
468
  this.reactiveVars = new Set();
461
469
  this.buffer = '';
462
470
  console.log(`${colors.green}Context cleared${colors.reset}`);
@@ -513,7 +521,7 @@ ${colors.cyan}Debug Toggles:${colors.reset}
513
521
 
514
522
  ${colors.cyan}Tips:${colors.reset}
515
523
  - Multi-line input is supported (press Enter mid-expression)
516
- - Full import() support: { x } = await import('./file.js')
524
+ - Import .rip files: { x } = await import('./file.rip')
517
525
  - Use Tab for history navigation
518
526
  - Previous results stored in _ variable
519
527
  - Use Ctrl+C to cancel multi-line input or exit
@@ -521,8 +529,7 @@ ${colors.cyan}Tips:${colors.reset}
521
529
  }
522
530
 
523
531
  printVars() {
524
- // Get all variables from the context
525
- const userVars = Object.keys(this.context).filter(k => k !== '_');
532
+ const userVars = Object.keys(this.vars).filter(k => k !== '_');
526
533
 
527
534
  if (userVars.length === 0) {
528
535
  console.log(`${colors.gray}No variables defined${colors.reset}`);
@@ -531,14 +538,13 @@ ${colors.cyan}Tips:${colors.reset}
531
538
 
532
539
  console.log(`${colors.bright}Defined variables:${colors.reset}`);
533
540
  userVars.forEach(key => {
534
- const value = this.context[key];
541
+ const value = this.vars[key];
535
542
  const preview = inspect(value, { colors: true, depth: 0, maxArrayLength: 3 });
536
543
  console.log(` ${colors.cyan}${key}${colors.reset} = ${preview}`);
537
544
  });
538
545
 
539
- // Show _ if it exists
540
- if (this.context._ !== undefined) {
541
- console.log(` ${colors.dim}${colors.cyan}_${colors.reset}${colors.dim} = ${inspect(this.context._, { colors: true, depth: 0 })}${colors.reset}`);
546
+ if (this.vars._ !== undefined) {
547
+ console.log(` ${colors.dim}${colors.cyan}_${colors.reset}${colors.dim} = ${inspect(this.vars._, { colors: true, depth: 0 })}${colors.reset}`);
542
548
  }
543
549
  }
544
550
 
@@ -560,26 +566,20 @@ ${colors.cyan}Tips:${colors.reset}
560
566
  if (fs.existsSync(this.historyFile)) {
561
567
  const historyData = fs.readFileSync(this.historyFile, 'utf-8');
562
568
  const lines = historyData.split('\n').filter(line => line.trim());
563
-
564
- // Load into this.history for tracking
565
569
  this.history = lines;
566
-
567
- // Also load into readline's history for arrow key navigation
568
- // Note: readline manages its own history, but we keep ours for .history command
569
- this.rl.history = [...lines].reverse(); // readline wants reverse order
570
+ this.rl.history = [...lines].reverse();
570
571
  }
571
572
  } catch (error) {
572
- // Silently ignore errors (first run, permission issues, etc.)
573
+ // Silently ignore errors
573
574
  }
574
575
  }
575
576
 
576
577
  saveHistory() {
577
578
  try {
578
- // Save last 1000 commands (prevent unlimited growth)
579
579
  const toSave = this.history.slice(-1000);
580
580
  fs.writeFileSync(this.historyFile, toSave.join('\n') + '\n', 'utf-8');
581
581
  } catch (error) {
582
- // Silently ignore errors (permission issues, etc.)
582
+ // Silently ignore errors
583
583
  }
584
584
  }
585
585
  }