rip-lang 3.16.0 → 3.16.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 +3 -4
- package/bin/rip +201 -18
- package/bin/rip-schema +175 -0
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +200 -19
- package/docs/RIP-DUCKDB.md +64 -1
- package/docs/RIP-INTRO.md +4 -4
- package/docs/RIP-LANG.md +36 -38
- package/docs/RIP-SCHEMA.md +1204 -364
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +4195 -966
- package/docs/dist/rip.min.js +1161 -284
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +2 -1
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
- package/docs/index.html +1 -1
- package/docs/ui/bundle.json +55 -55
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +1 -1
- package/docs/ui/index.html +1 -1
- package/package.json +15 -7
- package/rip-loader.js +59 -2
- package/src/AGENTS.md +43 -12
- package/src/browser.js +52 -11
- package/src/compiler.js +538 -80
- package/src/components.js +488 -48
- package/src/dts.js +80 -50
- package/src/grammar/README.md +29 -170
- package/src/grammar/grammar.rip +17 -12
- package/src/grammar/solar.rip +4 -17
- package/src/lexer.js +82 -32
- package/src/parser.js +229 -229
- package/src/schema/dts.js +328 -54
- package/src/schema/loader-server.js +2 -1
- package/src/schema/runtime-browser-stubs.js +20 -9
- package/src/schema/runtime-ddl.js +161 -44
- package/src/schema/runtime-migrate.js +681 -0
- package/src/schema/runtime-orm.js +698 -54
- package/src/schema/runtime-validate.js +808 -24
- package/src/schema/runtime.generated.js +2395 -135
- package/src/schema/schema.js +1054 -94
- package/src/typecheck.js +1610 -127
- package/src/types.js +90 -6
- package/src/grammar/lunar.rip +0 -2412
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="https://github.com/shreeve/rip-lang/commits/main"><img src="https://img.shields.io/badge/version-3.16.
|
|
12
|
+
<a href="https://github.com/shreeve/rip-lang/commits/main"><img src="https://img.shields.io/badge/version-3.16.1-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-1%2C436%2F1%2C436-brightgreen.svg" alt="Tests"></a>
|
|
15
15
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
|
|
@@ -38,7 +38,7 @@ get '/users/:id' -> # RESTful API endpoint, comma-less
|
|
|
38
38
|
|
|
39
39
|
**What makes Rip different:**
|
|
40
40
|
- **Modern output** — ES2022 with native classes, `?.`, `??`, modules
|
|
41
|
-
- **New operators** — `!`, `//`, `%%`, `=~`,
|
|
41
|
+
- **New operators** — `!`, `//`, `%%`, `=~`, `.new()`, and more
|
|
42
42
|
- **Reactive operators** — `:=`, `~=`, `~>` as language syntax
|
|
43
43
|
- **Optional types** — `::` annotations, `type` aliases, `.d.ts` emission
|
|
44
44
|
- **Zero dependencies** — everything included, even the parser generator
|
|
@@ -262,7 +262,6 @@ All use `globalThis` with `??=` — override any by redeclaring locally.
|
|
|
262
262
|
| `[-n]` (negative index) | `arr[-1]` | Last element via `.at()` |
|
|
263
263
|
| `*` (string repeat) | `"-" * 40` | String repeat via `.repeat()` |
|
|
264
264
|
| `<` `<=` (chained) | `1 < x < 10` | Chained comparisons |
|
|
265
|
-
| `\|>` (pipe) | `x \|> fn` or `x \|> fn(y)` | Pipe operator (first-arg insertion) |
|
|
266
265
|
| `not in` | `x not in arr` | Negated membership test |
|
|
267
266
|
| `not of` | `k not of obj` | Negated key existence |
|
|
268
267
|
| `.=` (method assign) | `x .= trim()` | `x = x.trim()` — compound method assignment |
|
|
@@ -341,7 +340,7 @@ Status = schema
|
|
|
341
340
|
# DB-backed model
|
|
342
341
|
User = schema :model
|
|
343
342
|
name! string
|
|
344
|
-
email
|
|
343
|
+
email! email @unique
|
|
345
344
|
@timestamps
|
|
346
345
|
@has_many Order
|
|
347
346
|
beforeValidation: -> @email = @email.toLowerCase()
|
package/bin/rip
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { readFileSync, readdirSync, writeFileSync, existsSync, statSync, unlinkSync } from 'fs';
|
|
3
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync, statSync, unlinkSync, lstatSync, readlinkSync, rmSync, mkdirSync, symlinkSync, realpathSync } from 'fs';
|
|
4
4
|
import { execSync, spawnSync, spawn } from 'child_process';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { dirname, basename, join } from 'path';
|
|
6
|
+
import { dirname, basename, join, resolve, relative } from 'path';
|
|
7
7
|
import { Compiler, formatError } from '../src/compiler.js';
|
|
8
8
|
// Side-effect imports — register CLI-side runtime providers. The browser
|
|
9
9
|
// bundle imports a different schema loader so server-only fragments
|
|
@@ -60,7 +60,12 @@ Options:
|
|
|
60
60
|
|
|
61
61
|
Subcommands:
|
|
62
62
|
rip check [dir] Type-check all .rip files in directory
|
|
63
|
-
rip check --
|
|
63
|
+
rip check --audit [pkgDir] Audit a typed package's public API surface for 'any' leaks
|
|
64
|
+
rip check --sourcemap [dir] Verify source-map round-trip for every identifier
|
|
65
|
+
(hover/go-to-def integrity). Useful for compiler work.
|
|
66
|
+
rip link [--quiet] After 'bun install', symlink @rip-lang/* deps to
|
|
67
|
+
local source (opt-in, for framework devs;
|
|
68
|
+
undo with 'bun install --force')
|
|
64
69
|
rip <name> [args] Run rip-<name> (repo bin/, node_modules, or PATH)
|
|
65
70
|
|
|
66
71
|
Configure via rip.json or package.json:
|
|
@@ -95,6 +100,121 @@ Shebang support:
|
|
|
95
100
|
`);
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
// `rip link` — opt-in override for framework maintainers, layered on top of a
|
|
104
|
+
// normal install. The canonical setup for any rip project is `bun install`
|
|
105
|
+
// (published packages, lockfile, transitive deps); `rip link` then redirects
|
|
106
|
+
// the @rip-lang/* deps to this CLI's own source checkout, by symlinking them
|
|
107
|
+
// into ./node_modules/@rip-lang/* (plus matching .bin shims). The `rip-lang`
|
|
108
|
+
// compiler/toolchain is pointed at source too, so the loader and VS Code LSP
|
|
109
|
+
// load the matching toolchain instead of a shadowing published tarball. Lets
|
|
110
|
+
// you edit framework source live without publishing.
|
|
111
|
+
//
|
|
112
|
+
// Requires `bun install` first — it overrides an existing install, it is not a
|
|
113
|
+
// substitute for one. Reversible: the symlinks live in node_modules (gitignored)
|
|
114
|
+
// and package.json stays clean semver, so `bun install --force` restores the
|
|
115
|
+
// published packages (a plain `bun install` sees the lockfile as satisfied and
|
|
116
|
+
// leaves the symlinks in place). No-op when no source tree is found (e.g. `rip`
|
|
117
|
+
// installed from npm without a dev checkout) — projects then just use installed
|
|
118
|
+
// packages.
|
|
119
|
+
//
|
|
120
|
+
// Source is this CLI's repo root (so it Just Works after `link-global`),
|
|
121
|
+
// overridable via RIP_SRC.
|
|
122
|
+
function ripLink(args) {
|
|
123
|
+
const quiet = args.includes('--quiet');
|
|
124
|
+
const log = (msg) => { if (!quiet) console.log(`[rip] link: ${msg}`); };
|
|
125
|
+
|
|
126
|
+
const rawSrc = process.env.RIP_SRC || join(__dirname, '..');
|
|
127
|
+
if (!existsSync(join(rawSrc, 'packages'))) {
|
|
128
|
+
log(`no rip-lang source at ${rawSrc} — using installed packages (set RIP_SRC to override)`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const src = realpathSync(rawSrc);
|
|
132
|
+
|
|
133
|
+
const cwd = process.cwd();
|
|
134
|
+
const pkgPath = join(cwd, 'package.json');
|
|
135
|
+
if (!existsSync(pkgPath)) {
|
|
136
|
+
console.error('[rip] link: no package.json in current directory');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
140
|
+
const deps = Object.keys(pkg.dependencies || {}).filter((d) => d.startsWith('@rip-lang/'));
|
|
141
|
+
if (deps.length === 0) {
|
|
142
|
+
log('no @rip-lang/* dependencies declared — nothing to link');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const nodeModules = join(cwd, 'node_modules');
|
|
147
|
+
if (!existsSync(nodeModules)) {
|
|
148
|
+
console.error('[rip] link: no node_modules found — run `bun install` first, then `rip link`.');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const binDir = join(nodeModules, '.bin');
|
|
152
|
+
|
|
153
|
+
const linkTo = (path, target, type = 'dir') => {
|
|
154
|
+
try {
|
|
155
|
+
if (lstatSync(path).isSymbolicLink() && readlinkSync(path) === target) return false;
|
|
156
|
+
rmSync(path, { recursive: true, force: true });
|
|
157
|
+
} catch {}
|
|
158
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
159
|
+
symlinkSync(target, path, type);
|
|
160
|
+
return true;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const linked = []; // { dep, bins: [names] } for each resolved package
|
|
164
|
+
let changed = false;
|
|
165
|
+
|
|
166
|
+
for (const dep of deps) {
|
|
167
|
+
const short = dep.slice('@rip-lang/'.length);
|
|
168
|
+
const target = join(src, 'packages', short);
|
|
169
|
+
if (!existsSync(target)) { log(`skip ${dep} — not found at ${target}`); continue; }
|
|
170
|
+
if (linkTo(join(nodeModules, '@rip-lang', short), target)) changed = true;
|
|
171
|
+
|
|
172
|
+
// Re-create .bin shims from the package's own `bin` field so `rip server`
|
|
173
|
+
// and friends resolve source binaries from this project's node_modules.
|
|
174
|
+
let meta = {};
|
|
175
|
+
try { meta = JSON.parse(readFileSync(join(target, 'package.json'), 'utf-8')); } catch {}
|
|
176
|
+
const binMap = typeof meta.bin === 'string' ? { [short]: meta.bin } : (meta.bin || {});
|
|
177
|
+
const bins = [];
|
|
178
|
+
for (const [name, rel] of Object.entries(binMap)) {
|
|
179
|
+
const binTarget = join(target, rel);
|
|
180
|
+
if (!existsSync(binTarget)) continue;
|
|
181
|
+
if (linkTo(join(binDir, name), binTarget, 'file')) changed = true;
|
|
182
|
+
bins.push(name);
|
|
183
|
+
}
|
|
184
|
+
linked.push({ dep, bins });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The compiler/toolchain itself. Every @rip-lang/* package depends on it,
|
|
188
|
+
// and the loader + VS Code LSP resolve it from the project's node_modules —
|
|
189
|
+
// so it must point at source too, otherwise a published `rip-lang` tarball
|
|
190
|
+
// shadows it (e.g. the LSP loading a typecheck.js without the latest API).
|
|
191
|
+
if (existsSync(join(nodeModules, 'rip-lang'))) {
|
|
192
|
+
if (linkTo(join(nodeModules, 'rip-lang'), src)) changed = true;
|
|
193
|
+
linked.push({ dep: 'rip-lang', bins: [], label: 'compiler' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (quiet || linked.length === 0) {
|
|
197
|
+
if (linked.length === 0) log('no @rip-lang/* packages resolved to source');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
202
|
+
const tilde = (p) => home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
203
|
+
const tty = process.stdout.isTTY;
|
|
204
|
+
const dim = (s) => tty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
205
|
+
const bold = (s) => tty ? `\x1b[1m${s}\x1b[0m` : s;
|
|
206
|
+
|
|
207
|
+
const n = linked.length;
|
|
208
|
+
const verb = changed ? 'linked' : 'already linked';
|
|
209
|
+
console.log(`${bold('rip link')} ${dim('·')} ${verb} ${n} package${n === 1 ? '' : 's'} ${dim('→ ' + tilde(src))}`);
|
|
210
|
+
const width = Math.max(...linked.map((l) => l.dep.length));
|
|
211
|
+
for (const { dep, bins, label } of linked) {
|
|
212
|
+
const tag = label || (bins.length ? `bin: ${bins.join(', ')}` : '');
|
|
213
|
+
const line = tag ? `${dep.padEnd(width)}${dim(` ${tag}`)}` : dep;
|
|
214
|
+
console.log(` ${dim('•')} ${line}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
98
218
|
async function main() {
|
|
99
219
|
const args = process.argv.slice(2);
|
|
100
220
|
|
|
@@ -125,12 +245,21 @@ async function main() {
|
|
|
125
245
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
126
246
|
const scopeDir = join(home, '.bun', 'install', 'global', 'node_modules', '@rip-lang');
|
|
127
247
|
if (existsSync(scopeDir) && statSync(scopeDir).isDirectory()) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
248
|
+
// Sort by the displayed package name (e.g. `@rip-lang/ui`,
|
|
249
|
+
// `vscode-rip`) rather than the on-disk directory entry — most
|
|
250
|
+
// entries match either way, but a package whose `name` field
|
|
251
|
+
// diverges from its directory (the VS Code extension lives in
|
|
252
|
+
// packages/vscode/ but publishes as `vscode-rip`) would
|
|
253
|
+
// otherwise sort by its directory and look misplaced.
|
|
254
|
+
const pkgs = readdirSync(scopeDir)
|
|
255
|
+
.map((name) => { try { return JSON.parse(readFileSync(join(scopeDir, name, 'package.json'), 'utf-8')); } catch { return null; } })
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
258
|
+
if (pkgs.length) {
|
|
259
|
+
console.log();
|
|
260
|
+
for (const pkg of pkgs) {
|
|
132
261
|
console.log(` ${pkg.name.padEnd(24)} ${pkg.version}`);
|
|
133
|
-
}
|
|
262
|
+
}
|
|
134
263
|
}
|
|
135
264
|
}
|
|
136
265
|
} catch {}
|
|
@@ -140,14 +269,27 @@ async function main() {
|
|
|
140
269
|
// --- Built-in subcommands ---
|
|
141
270
|
|
|
142
271
|
if (args[0] === 'check') {
|
|
143
|
-
const { runCheck } = await import('../src/typecheck.js');
|
|
144
272
|
const checkArgs = args.slice(1);
|
|
273
|
+
const VALID_FLAGS = new Set(['--audit', '--sourcemap']);
|
|
274
|
+
const unknown = checkArgs.filter(a => a.startsWith('-') && !VALID_FLAGS.has(a));
|
|
275
|
+
if (unknown.length > 0) {
|
|
276
|
+
console.error(`rip check: unknown flag${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
|
|
277
|
+
console.error(`Valid flags: ${[...VALID_FLAGS].join(', ')}`);
|
|
278
|
+
process.exit(2);
|
|
279
|
+
}
|
|
145
280
|
const dir = checkArgs.find(a => !a.startsWith('-')) || '.';
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
150
|
-
process.exit(
|
|
281
|
+
const wantAudit = checkArgs.includes('--audit');
|
|
282
|
+
const wantSourcemap = checkArgs.includes('--sourcemap');
|
|
283
|
+
const { runCheck, runAudit } = await import('../src/typecheck.js');
|
|
284
|
+
const checkCode = await runCheck(dir, { sourceMapAudit: wantSourcemap });
|
|
285
|
+
if (!wantAudit) process.exit(checkCode);
|
|
286
|
+
const auditCode = await runAudit(dir);
|
|
287
|
+
process.exit(checkCode || auditCode);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (args[0] === 'link') {
|
|
291
|
+
ripLink(args.slice(1));
|
|
292
|
+
process.exit(0);
|
|
151
293
|
}
|
|
152
294
|
|
|
153
295
|
// --- Subcommand dispatch: rip <name> → rip-<name> ---
|
|
@@ -158,6 +300,15 @@ async function main() {
|
|
|
158
300
|
const name = candidate;
|
|
159
301
|
const subArgs = args.slice(scriptFileIndex + 1);
|
|
160
302
|
|
|
303
|
+
// 0. Sibling subcommand in this CLI's own bin/ — works from any
|
|
304
|
+
// cwd (getRepoRoot below resolves the CWD's git checkout, which
|
|
305
|
+
// is unrelated when running `rip schema …` inside an app dir).
|
|
306
|
+
const selfSibling = join(__dirname, `rip-${name}`);
|
|
307
|
+
if (existsSync(selfSibling)) {
|
|
308
|
+
const r = spawnSync(selfSibling, subArgs, { stdio: 'inherit', env: process.env });
|
|
309
|
+
process.exit(r.status ?? 1);
|
|
310
|
+
}
|
|
311
|
+
|
|
161
312
|
const repoRoot = getRepoRoot();
|
|
162
313
|
if (repoRoot) {
|
|
163
314
|
// 1. Repo package: bin/rip-<name>
|
|
@@ -197,13 +348,32 @@ async function main() {
|
|
|
197
348
|
}
|
|
198
349
|
}
|
|
199
350
|
|
|
200
|
-
// 5.
|
|
351
|
+
// 5. Nearest node_modules/.bin walking up from the cwd. `getRepoRoot`
|
|
352
|
+
// uses `git rev-parse --show-toplevel`, which returns the OUTERMOST git
|
|
353
|
+
// repo — wrong when a project lives in a subdirectory of a larger git
|
|
354
|
+
// repo: step 4 then probes the outer repo's node_modules and misses the
|
|
355
|
+
// project's own declared bins. Resolve the bin the way Node resolves a
|
|
356
|
+
// dependency — nearest node_modules up the tree from the cwd — which is
|
|
357
|
+
// correct for nested projects and standalone consumers alike.
|
|
358
|
+
let nmDir = process.cwd();
|
|
359
|
+
while (true) {
|
|
360
|
+
const nmBin = join(nmDir, 'node_modules', '.bin', `rip-${name}`);
|
|
361
|
+
if (existsSync(nmBin)) {
|
|
362
|
+
const r = spawnSync(nmBin, subArgs, { stdio: 'inherit', env: process.env });
|
|
363
|
+
process.exit(r.status ?? 1);
|
|
364
|
+
}
|
|
365
|
+
const parent = dirname(nmDir);
|
|
366
|
+
if (parent === nmDir) break;
|
|
367
|
+
nmDir = parent;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 6. Global PATH: rip-<name>
|
|
201
371
|
const pathResult = spawnSync(`rip-${name}`, subArgs, { stdio: 'inherit', env: process.env });
|
|
202
372
|
if (pathResult.error?.code !== 'ENOENT') {
|
|
203
373
|
process.exit(pathResult.status ?? 1);
|
|
204
374
|
}
|
|
205
375
|
|
|
206
|
-
//
|
|
376
|
+
// 7. Not found
|
|
207
377
|
console.error(`rip: unknown command '${name}'\n\nRun 'rip --help' for usage.`);
|
|
208
378
|
process.exit(1);
|
|
209
379
|
}
|
|
@@ -335,8 +505,21 @@ async function main() {
|
|
|
335
505
|
}
|
|
336
506
|
|
|
337
507
|
if (showShadow) {
|
|
338
|
-
const { compileForCheck } = await import('../src/typecheck.js');
|
|
339
|
-
|
|
508
|
+
const { compileForCheck, readProjectConfig, globToRegex } = await import('../src/typecheck.js');
|
|
509
|
+
// Honor the project's `rip` config so the shadow matches what `rip
|
|
510
|
+
// check` actually does. Without `checkAll`, an unannotated file gets a
|
|
511
|
+
// `// @ts-nocheck` it wouldn't get under the project's real settings.
|
|
512
|
+
const config = readProjectConfig(inputFile ? dirname(resolve(inputFile)) : process.cwd());
|
|
513
|
+
const checkAll = config.checkAll === true;
|
|
514
|
+
// An excluded file is skipped entirely — surface that, since the shadow
|
|
515
|
+
// below reflects nocheck logic that never runs for excluded files.
|
|
516
|
+
if (inputFile && config._configDir && Array.isArray(config.exclude)) {
|
|
517
|
+
const rel = relative(config._configDir, resolve(inputFile));
|
|
518
|
+
if (config.exclude.map(globToRegex).some(p => p.test(rel)) && !quiet) {
|
|
519
|
+
console.log(`// NOTE: ${rel} matches rip.exclude — \`rip check\` skips this file entirely.\n`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const entry = compileForCheck(inputFile || '<stdin>', source, new Compiler(), { checkAll });
|
|
340
523
|
if (!quiet) console.log(`// == Shadow TypeScript (what \`rip check\` sees) == //\n`);
|
|
341
524
|
console.log(entry.tsContent);
|
|
342
525
|
}
|
package/bin/rip-schema
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
//
|
|
3
|
+
// rip-schema — schema evolution CLI (`rip schema …`).
|
|
4
|
+
//
|
|
5
|
+
// rip schema status [entry.rip] [--dir DIR]
|
|
6
|
+
// rip schema plan [entry.rip]
|
|
7
|
+
// rip schema make <name> [entry.rip] [--dir DIR] [--allow-lossy] [--allow-destructive]
|
|
8
|
+
// rip schema migrate [entry.rip] [--dir DIR] [--repair]
|
|
9
|
+
//
|
|
10
|
+
// `entry.rip` is a file whose import registers every :model schema
|
|
11
|
+
// (your models file — it may also call connect() to point the adapter
|
|
12
|
+
// at the database). When omitted, conventional locations are tried.
|
|
13
|
+
//
|
|
14
|
+
// Database connection comes from the entry file's own connect() call,
|
|
15
|
+
// or from RIP_DB_URL / RIP_DB_TOKEN for the default duckdb-harbor
|
|
16
|
+
// adapter.
|
|
17
|
+
//
|
|
18
|
+
// Migration files are plain SQL — numbered, hand-editable, checked into
|
|
19
|
+
// git (default directory: ./migrations). `make` writes them from the
|
|
20
|
+
// model-vs-database diff; `migrate` applies pending ones in order and
|
|
21
|
+
// records (version, name, checksum) in the `_rip_migrations` table.
|
|
22
|
+
|
|
23
|
+
// Registers the .rip module loader AND the full (migration-mode) schema
|
|
24
|
+
// runtime — globalThis.schema gains plan/status/make/migrate/introspect.
|
|
25
|
+
import '../rip-loader.js';
|
|
26
|
+
|
|
27
|
+
import { existsSync } from 'fs';
|
|
28
|
+
import { resolve } from 'path';
|
|
29
|
+
|
|
30
|
+
const ENTRY_CANDIDATES = [
|
|
31
|
+
'models.rip', 'api/models.rip', 'app/models.rip', 'db/models.rip', 'src/models.rip',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
const cmd = args[0];
|
|
36
|
+
|
|
37
|
+
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
38
|
+
usage(0);
|
|
39
|
+
} else if (!['status', 'plan', 'make', 'migrate'].includes(cmd)) {
|
|
40
|
+
console.error(`rip schema: unknown subcommand '${cmd}'\n`);
|
|
41
|
+
usage(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rest = args.slice(1);
|
|
45
|
+
const flags = {
|
|
46
|
+
dir: 'migrations',
|
|
47
|
+
allowLossy: false,
|
|
48
|
+
allowDestructive: false,
|
|
49
|
+
repair: false,
|
|
50
|
+
};
|
|
51
|
+
const positional = [];
|
|
52
|
+
for (let i = 0; i < rest.length; i++) {
|
|
53
|
+
const a = rest[i];
|
|
54
|
+
if (a === '--dir') flags.dir = rest[++i];
|
|
55
|
+
else if (a === '--allow-lossy') flags.allowLossy = true;
|
|
56
|
+
else if (a === '--allow-destructive') flags.allowDestructive = true;
|
|
57
|
+
else if (a === '--repair') flags.repair = true;
|
|
58
|
+
else if (a.startsWith('-')) die(`unknown flag: ${a}`);
|
|
59
|
+
else positional.push(a);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// `make` takes a migration name first; every command takes an optional
|
|
63
|
+
// models entry. Disambiguate by file existence: a positional that names
|
|
64
|
+
// an existing file is the entry, anything else (for make) is the name.
|
|
65
|
+
let name = null;
|
|
66
|
+
let entry = null;
|
|
67
|
+
for (const p of positional) {
|
|
68
|
+
if (existsSync(p) && /\.(rip|js|ts)$/.test(p)) entry = p;
|
|
69
|
+
else if (cmd === 'make' && !name) name = p;
|
|
70
|
+
else die(`unexpected argument: ${p} (no such file)`);
|
|
71
|
+
}
|
|
72
|
+
if (cmd === 'make' && !name) die("usage: rip schema make <name> [entry.rip] — a migration name is required");
|
|
73
|
+
if (!entry) entry = ENTRY_CANDIDATES.find(c => existsSync(c)) || null;
|
|
74
|
+
if (!entry) {
|
|
75
|
+
die(
|
|
76
|
+
'no models entry found. Pass one explicitly (rip schema ' + cmd + ' [name] path/to/models.rip)\n' +
|
|
77
|
+
'or create one of: ' + ENTRY_CANDIDATES.join(', '));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Importing the entry registers every :model it declares (directly or
|
|
81
|
+
// transitively) and runs any connect() it performs.
|
|
82
|
+
await import(resolve(entry));
|
|
83
|
+
|
|
84
|
+
const schema = globalThis.schema;
|
|
85
|
+
if (!schema || typeof schema.plan !== 'function') {
|
|
86
|
+
die('schema runtime did not load — does the entry file declare or import any schemas?');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const color = (c, s) => process.stdout.isTTY ? `\x1b[${c}m${s}\x1b[0m` : s;
|
|
90
|
+
const dim = (s) => color('2', s);
|
|
91
|
+
const CLASS_COLOR = { safe: '32', lossy: '33', destructive: '31', blocked: '31;1' };
|
|
92
|
+
const tag = (cls) => color(CLASS_COLOR[cls] || '0', `[${cls}]`);
|
|
93
|
+
|
|
94
|
+
function printSteps(steps) {
|
|
95
|
+
if (!steps.length) {
|
|
96
|
+
console.log(color('32', '✓ database matches the declared models — no changes'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const s of steps) {
|
|
100
|
+
console.log(`${tag(s.class)} ${s.kind} ${s.table}`);
|
|
101
|
+
for (const n of s.notes) console.log(dim(` ${n}`));
|
|
102
|
+
for (const line of s.sql) console.log(` ${line}`);
|
|
103
|
+
}
|
|
104
|
+
const counts = {};
|
|
105
|
+
for (const s of steps) counts[s.class] = (counts[s.class] || 0) + 1;
|
|
106
|
+
console.log('\n' + Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', '));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (cmd === 'plan') {
|
|
111
|
+
printSteps(await schema.plan());
|
|
112
|
+
|
|
113
|
+
} else if (cmd === 'status') {
|
|
114
|
+
const st = await schema.status({ dir: flags.dir });
|
|
115
|
+
console.log(`applied: ${st.applied.length ? st.applied.map(a => a.version + '_' + a.name).join(', ') : '(none)'}`);
|
|
116
|
+
console.log(`pending: ${st.pending.length ? st.pending.map(f => f.version + '_' + f.name).join(', ') : '(none)'}`);
|
|
117
|
+
if (st.mismatched.length) {
|
|
118
|
+
console.log(color('31', `edited after apply: ${st.mismatched.join(', ')} — restore the files or migrate --repair`));
|
|
119
|
+
}
|
|
120
|
+
console.log('');
|
|
121
|
+
if (st.steps.length && !st.pending.length && (st.applied.length || st.files.length)) {
|
|
122
|
+
console.log(color('33', 'drift: the database differs from the models in ways no pending migration explains'));
|
|
123
|
+
}
|
|
124
|
+
printSteps(st.steps);
|
|
125
|
+
|
|
126
|
+
} else if (cmd === 'make') {
|
|
127
|
+
const out = await schema.make(name, {
|
|
128
|
+
dir: flags.dir,
|
|
129
|
+
allowLossy: flags.allowLossy,
|
|
130
|
+
allowDestructive: flags.allowDestructive,
|
|
131
|
+
});
|
|
132
|
+
if (!out) {
|
|
133
|
+
console.log(color('32', '✓ no changes — nothing to write'));
|
|
134
|
+
} else {
|
|
135
|
+
printSteps(out.steps);
|
|
136
|
+
console.log(`\nwrote ${out.file}`);
|
|
137
|
+
console.log(dim('review the file, then apply with: rip schema migrate'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
} else if (cmd === 'migrate') {
|
|
141
|
+
const out = await schema.migrate({ dir: flags.dir, repair: flags.repair });
|
|
142
|
+
if (!out.ran.length) console.log(color('32', '✓ no pending migrations'));
|
|
143
|
+
else for (const r of out.ran) console.log(`applied ${r}`);
|
|
144
|
+
}
|
|
145
|
+
process.exit(0);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
die(e?.message || String(e));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function usage(code) {
|
|
151
|
+
console.log(`\
|
|
152
|
+
rip schema — diff declared :model schemas against the database and manage migrations.
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
rip schema status [entry.rip] [--dir DIR] applied / pending / drift + the current plan
|
|
156
|
+
rip schema plan [entry.rip] print the classified diff (no files touched)
|
|
157
|
+
rip schema make <name> [entry.rip] [--dir DIR] write migrations/NNNN_<name>.sql from the diff
|
|
158
|
+
[--allow-lossy] [--allow-destructive]
|
|
159
|
+
rip schema migrate [entry.rip] [--dir DIR] apply pending migration files in order
|
|
160
|
+
[--repair]
|
|
161
|
+
|
|
162
|
+
entry.rip file that declares/imports every :model (default: ${ENTRY_CANDIDATES.join(' | ')})
|
|
163
|
+
--dir DIR migrations directory (default: migrations)
|
|
164
|
+
--allow-lossy include steps that may lose data on existing rows (type changes, SET NOT NULL)
|
|
165
|
+
--allow-destructive include DROP TABLE / DROP COLUMN steps
|
|
166
|
+
--repair re-record checksums for applied migrations whose files changed
|
|
167
|
+
|
|
168
|
+
Connection: the entry's connect() call, or RIP_DB_URL / RIP_DB_TOKEN.`);
|
|
169
|
+
process.exit(code);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function die(msg) {
|
|
173
|
+
console.error(`rip schema: ${msg}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
package/docs/AGENTS.md
CHANGED
|
@@ -38,6 +38,6 @@ App.mount()
|
|
|
38
38
|
- `demo.html` and `charts.html` — dashboard demos
|
|
39
39
|
- `sierpinski.html` — CDN demo
|
|
40
40
|
- `example/index.html` and `results/index.html` — app launchers / examples. `example/index.json` is generated from `docs/demo/` via `bun run bundle:demo` (the source-of-truth lives in `docs/demo/`, the JSON is the deployable artifact).
|
|
41
|
-
- `ui/index.html` — widget gallery. `ui/bundle.json` is generated from `packages/ui/browser/components/` via `bun run bundle:ui` (auto-runs as part of `bun run build`). The source-of-truth is the workspace package; the JSON is the deployable artifact. The gallery loads the bundle at boot via `data-src="bundle.json"` and reads view-source text synchronously from the in-memory components store (`window.__RIP__.components.read("
|
|
41
|
+
- `ui/index.html` — widget gallery. `ui/bundle.json` is generated from `packages/ui/browser/components/` via `bun run bundle:ui` (auto-runs as part of `bun run build`). The source-of-truth is the workspace package; the JSON is the deployable artifact. The gallery loads the bundle at boot via `data-src="bundle.json"` and reads view-source text synchronously from the in-memory components store (`window.__RIP__.components.read("_pkg/ui/<id>.rip")`) — no per-component fetches.
|
|
42
42
|
|
|
43
43
|
Static demos can be opened via `file://`. The playground and example app require `bun run serve`.
|