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 +24 -0
- package/README.md +1 -1
- package/bin/rip +6 -1
- package/docs/WHY-YES-RIP.md +3 -3
- package/docs/dist/rip.browser.js +2 -2
- package/docs/dist/rip.browser.min.js +1 -1
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +1 -1
- package/src/repl.js +175 -175
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.
|
|
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
|
-
|
|
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
|
|
package/docs/WHY-YES-RIP.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
755
|
+
**Version 2.7.2. Available now. Clone and go.**
|
|
756
756
|
|
|
757
757
|
This approach is ready. Give it a try.
|
package/docs/dist/rip.browser.js
CHANGED
|
@@ -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.
|
|
7492
|
-
var BUILD_DATE = "2026-02-03@
|
|
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.
|
|
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
package/src/repl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rip REPL - Interactive Read-Eval-Print Loop
|
|
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
|
-
* -
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
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
|
-
|
|
287
|
-
if (trimmed.endsWith('
|
|
288
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
//
|
|
356
|
-
const
|
|
357
|
-
|
|
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
|
-
//
|
|
362
|
-
const
|
|
363
|
-
|
|
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
|
-
//
|
|
366
|
-
|
|
389
|
+
// Restore existing variables and remove duplicate declarations
|
|
390
|
+
const existingVars = Object.keys(this.vars);
|
|
367
391
|
for (const v of existingVars) {
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
//
|
|
373
|
-
const
|
|
396
|
+
// Build restore code (get vars from __vars)
|
|
397
|
+
const restoreCode = existingVars
|
|
374
398
|
.filter(k => k !== '_')
|
|
375
|
-
.map(
|
|
399
|
+
.map(v => `let ${v} = __vars['${v}'];`)
|
|
376
400
|
.join('\n');
|
|
377
401
|
|
|
378
|
-
//
|
|
379
|
-
const
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
//
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
${
|
|
423
|
+
${saveCode}
|
|
424
|
+
export { __result };
|
|
402
425
|
`;
|
|
403
426
|
|
|
404
|
-
//
|
|
405
|
-
const
|
|
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
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
540
|
-
|
|
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
|
|
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
|
|
582
|
+
// Silently ignore errors
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
585
|
}
|