rip-lang 3.16.0 → 3.16.1
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 +1 -1
- package/bin/rip +162 -10
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +109 -17
- package/docs/RIP-LANG.md +4 -5
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +933 -338
- package/docs/dist/rip.min.js +209 -204
- 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/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/ui/bundle.json +55 -55
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/index.html +1 -1
- package/package.json +9 -4
- package/rip-loader.js +59 -2
- package/src/AGENTS.md +5 -5
- package/src/browser.js +52 -11
- package/src/compiler.js +318 -44
- package/src/components.js +178 -39
- package/src/dts.js +62 -47
- package/src/lexer.js +58 -15
- package/src/schema/schema.js +5 -5
- package/src/typecheck.js +1355 -100
- package/src/types.js +85 -5
- /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>
|
package/bin/rip
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import { dirname, basename, join } from 'path';
|
|
@@ -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
|
|
|
@@ -140,14 +260,27 @@ async function main() {
|
|
|
140
260
|
// --- Built-in subcommands ---
|
|
141
261
|
|
|
142
262
|
if (args[0] === 'check') {
|
|
143
|
-
const { runCheck } = await import('../src/typecheck.js');
|
|
144
263
|
const checkArgs = args.slice(1);
|
|
264
|
+
const VALID_FLAGS = new Set(['--audit', '--sourcemap']);
|
|
265
|
+
const unknown = checkArgs.filter(a => a.startsWith('-') && !VALID_FLAGS.has(a));
|
|
266
|
+
if (unknown.length > 0) {
|
|
267
|
+
console.error(`rip check: unknown flag${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
|
|
268
|
+
console.error(`Valid flags: ${[...VALID_FLAGS].join(', ')}`);
|
|
269
|
+
process.exit(2);
|
|
270
|
+
}
|
|
145
271
|
const dir = checkArgs.find(a => !a.startsWith('-')) || '.';
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
150
|
-
process.exit(
|
|
272
|
+
const wantAudit = checkArgs.includes('--audit');
|
|
273
|
+
const wantSourcemap = checkArgs.includes('--sourcemap');
|
|
274
|
+
const { runCheck, runAudit } = await import('../src/typecheck.js');
|
|
275
|
+
const checkCode = await runCheck(dir, { sourceMapAudit: wantSourcemap });
|
|
276
|
+
if (!wantAudit) process.exit(checkCode);
|
|
277
|
+
const auditCode = await runAudit(dir);
|
|
278
|
+
process.exit(checkCode || auditCode);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (args[0] === 'link') {
|
|
282
|
+
ripLink(args.slice(1));
|
|
283
|
+
process.exit(0);
|
|
151
284
|
}
|
|
152
285
|
|
|
153
286
|
// --- Subcommand dispatch: rip <name> → rip-<name> ---
|
|
@@ -197,13 +330,32 @@ async function main() {
|
|
|
197
330
|
}
|
|
198
331
|
}
|
|
199
332
|
|
|
200
|
-
// 5.
|
|
333
|
+
// 5. Nearest node_modules/.bin walking up from the cwd. `getRepoRoot`
|
|
334
|
+
// uses `git rev-parse --show-toplevel`, which returns the OUTERMOST git
|
|
335
|
+
// repo — wrong when a project lives in a subdirectory of a larger git
|
|
336
|
+
// repo: step 4 then probes the outer repo's node_modules and misses the
|
|
337
|
+
// project's own declared bins. Resolve the bin the way Node resolves a
|
|
338
|
+
// dependency — nearest node_modules up the tree from the cwd — which is
|
|
339
|
+
// correct for nested projects and standalone consumers alike.
|
|
340
|
+
let nmDir = process.cwd();
|
|
341
|
+
while (true) {
|
|
342
|
+
const nmBin = join(nmDir, 'node_modules', '.bin', `rip-${name}`);
|
|
343
|
+
if (existsSync(nmBin)) {
|
|
344
|
+
const r = spawnSync(nmBin, subArgs, { stdio: 'inherit', env: process.env });
|
|
345
|
+
process.exit(r.status ?? 1);
|
|
346
|
+
}
|
|
347
|
+
const parent = dirname(nmDir);
|
|
348
|
+
if (parent === nmDir) break;
|
|
349
|
+
nmDir = parent;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 6. Global PATH: rip-<name>
|
|
201
353
|
const pathResult = spawnSync(`rip-${name}`, subArgs, { stdio: 'inherit', env: process.env });
|
|
202
354
|
if (pathResult.error?.code !== 'ENOENT') {
|
|
203
355
|
process.exit(pathResult.status ?? 1);
|
|
204
356
|
}
|
|
205
357
|
|
|
206
|
-
//
|
|
358
|
+
// 7. Not found
|
|
207
359
|
console.error(`rip: unknown command '${name}'\n\nRun 'rip --help' for usage.`);
|
|
208
360
|
process.exit(1);
|
|
209
361
|
}
|
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`.
|
package/docs/RIP-APP.md
CHANGED
|
@@ -24,21 +24,37 @@ shaped the way it is, and how to use it well.
|
|
|
24
24
|
|
|
25
25
|
# Contents
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
- [Rip App — Application Framework](#rip-app--application-framework)
|
|
28
|
+
- [Contents](#contents)
|
|
29
|
+
- [1. The four-layer architecture](#1-the-four-layer-architecture)
|
|
30
|
+
- [2. Quick start](#2-quick-start)
|
|
31
|
+
- [Real apps: bundles + file-based routing](#real-apps-bundles--file-based-routing)
|
|
32
|
+
- [3. The subsystems](#3-the-subsystems)
|
|
33
|
+
- [Stash](#stash)
|
|
34
|
+
- [createResource](#createresource)
|
|
35
|
+
- [Timing helpers](#timing-helpers)
|
|
36
|
+
- [Components store](#components-store)
|
|
37
|
+
- [createRouter](#createrouter)
|
|
38
|
+
- [createRenderer](#createrenderer)
|
|
39
|
+
- [launch](#launch)
|
|
40
|
+
- [ARIA helpers](#aria-helpers)
|
|
41
|
+
- [4. Lifecycle invariants](#4-lifecycle-invariants)
|
|
42
|
+
- [Component lifecycle order](#component-lifecycle-order)
|
|
43
|
+
- [User hooks](#user-hooks)
|
|
44
|
+
- [Effect ownership](#effect-ownership)
|
|
45
|
+
- [Effect cleanup-on-rerun](#effect-cleanup-on-rerun)
|
|
46
|
+
- [Parent chain (for context)](#parent-chain-for-context)
|
|
47
|
+
- [Layout and page parentage](#layout-and-page-parentage)
|
|
48
|
+
- [Factory blocks (for/if in render)](#factory-blocks-forif-in-render)
|
|
49
|
+
- [Keyed list reconciliation](#keyed-list-reconciliation)
|
|
50
|
+
- [5. Async effects](#5-async-effects)
|
|
51
|
+
- [6. Gotchas](#6-gotchas)
|
|
52
|
+
- [The bundle boundary matters](#the-bundle-boundary-matters)
|
|
53
|
+
- [Render-template name shadowing (fixed)](#render-template-name-shadowing-fixed)
|
|
54
|
+
- [Nested `for` loops can both name `i` (fixed)](#nested-for-loops-can-both-name-i-fixed)
|
|
55
|
+
- [Snapshot tests are brittle](#snapshot-tests-are-brittle)
|
|
56
|
+
- [No browser e2e tests](#no-browser-e2e-tests)
|
|
57
|
+
- [7. When NOT to use Rip App](#7-when-not-to-use-rip-app)
|
|
42
58
|
|
|
43
59
|
---
|
|
44
60
|
|
|
@@ -185,7 +201,7 @@ A deep reactive proxy with path navigation. Single-app state,
|
|
|
185
201
|
JSON-persistable, fine-grained signal subscription per key.
|
|
186
202
|
|
|
187
203
|
```rip
|
|
188
|
-
app =
|
|
204
|
+
app = createStash
|
|
189
205
|
user:
|
|
190
206
|
name: "Alice"
|
|
191
207
|
prefs:
|
|
@@ -205,7 +221,7 @@ app.keys 'user' # → ['name', 'prefs']
|
|
|
205
221
|
app.join 'user', email: "..." # shallow merge
|
|
206
222
|
|
|
207
223
|
# Use raw object underneath
|
|
208
|
-
plain =
|
|
224
|
+
plain = unwrapStash(app) # back to a plain JS object
|
|
209
225
|
```
|
|
210
226
|
|
|
211
227
|
**Single-stash policy**: Rip App assumes one stash per page (the one
|
|
@@ -291,6 +307,7 @@ router = createRouter components,
|
|
|
291
307
|
|
|
292
308
|
router.push '/users/42?tab=settings'
|
|
293
309
|
router.replace '/login'
|
|
310
|
+
router.push '/cart', noScroll: true # don't reset scroll on this nav
|
|
294
311
|
router.back()
|
|
295
312
|
router.forward()
|
|
296
313
|
|
|
@@ -316,6 +333,65 @@ The renderer uses `router.current` to drive its mount effect. Each
|
|
|
316
333
|
field is also a separate signal so subscribers can track only what
|
|
317
334
|
they care about.
|
|
318
335
|
|
|
336
|
+
#### Anchor opt-outs and active-link styling
|
|
337
|
+
|
|
338
|
+
The router intercepts plain `<a>` clicks at the document level. Two
|
|
339
|
+
per-anchor attributes adjust that behavior:
|
|
340
|
+
|
|
341
|
+
| Attribute | Effect |
|
|
342
|
+
| ----------------------- | ------------------------------------------------------------------------------------- |
|
|
343
|
+
| `data-router-ignore` | Skip SPA interception entirely. The browser performs a full navigation. |
|
|
344
|
+
| `data-router-noscroll` | Take the SPA navigation, but don't reset scroll to `(0, 0)`. |
|
|
345
|
+
|
|
346
|
+
Anchors with `target="_blank"`, `[download]`, cross-origin hrefs, or
|
|
347
|
+
hrefs outside `base` are also skipped automatically.
|
|
348
|
+
|
|
349
|
+
**Active-link highlighting.** On every navigation the router walks
|
|
350
|
+
in-document anchors and sets `aria-current` on those that match the
|
|
351
|
+
current path:
|
|
352
|
+
|
|
353
|
+
- exact match → `aria-current="page"`
|
|
354
|
+
- prefix match (`/blog` on `/blog/123`) → `aria-current="true"`
|
|
355
|
+
- otherwise → attribute removed (only if the router set it)
|
|
356
|
+
|
|
357
|
+
Style it with attribute selectors — no per-link boilerplate needed:
|
|
358
|
+
|
|
359
|
+
```css
|
|
360
|
+
nav a[aria-current="page"] { color: red; font-weight: bold; }
|
|
361
|
+
nav a[aria-current="true"] { color: red; }
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Setting `aria-current` manually on an anchor wins — the router only
|
|
365
|
+
touches values it set itself.
|
|
366
|
+
|
|
367
|
+
**Scroll restoration.** New navigations (`push` or a link click)
|
|
368
|
+
reset scroll to `(0, 0)`. Back/forward (`popstate`) restores the
|
|
369
|
+
scroll position the page had when you left it. Same-document fragment
|
|
370
|
+
links (`#section`) defer to the browser.
|
|
371
|
+
|
|
372
|
+
**Typed routes (compile-time).** In a typed project (one with
|
|
373
|
+
`rip.strict: true` or `::` annotations), `rip check` synthesizes a
|
|
374
|
+
`__RipRoutes` union from the file tree under `app/routes/` and
|
|
375
|
+
threads it through three places:
|
|
376
|
+
|
|
377
|
+
| Place | Type | Catches |
|
|
378
|
+
| ---------------------------------- | --------------------------------------------------------------------- | ---------------------------------- |
|
|
379
|
+
| `<a href: "...">` in render blocks | `__RipRoutes` for `/`-prefixed literals; any string otherwise | Typos in known routes |
|
|
380
|
+
| `router.push url, opts?` | `__RipRoutes` (replaces base `string`) | Typos in programmatic navigation |
|
|
381
|
+
| `@params` in `routes/[id].rip` | `{ id: string }` (replaces `Record<string, string>`) | Typos like `@params.bogus` |
|
|
382
|
+
|
|
383
|
+
Anchor `href` uses a `const`-generic conditional: a literal starting
|
|
384
|
+
with `/` must satisfy `__RipRoutes`, while external schemes
|
|
385
|
+
(`https://`, `mailto:`, `tel:`), fragments (`#anchor`), and dynamic
|
|
386
|
+
`string` values fall through unchecked. Typos like `<a href: "/crat">`
|
|
387
|
+
produce a single-line error naming the valid routes.
|
|
388
|
+
`router.replace` is deliberately left at `string` — it's commonly
|
|
389
|
+
used to mutate the current URL with query strings, where the built
|
|
390
|
+
value can't satisfy a literal-route union. Catch-all routes
|
|
391
|
+
(`[...rest].rip`) are excluded from `__RipRoutes` — they're runtime
|
|
392
|
+
404 fallbacks, not navigation targets, so including them as
|
|
393
|
+
`/${string}` would defeat typo-catching for every other route.
|
|
394
|
+
|
|
319
395
|
### createRenderer
|
|
320
396
|
|
|
321
397
|
The render loop. Subscribes to `router.current`, mounts/unmounts
|
|
@@ -422,6 +498,8 @@ mount(target) ← only the renderer calls this directly
|
|
|
422
498
|
Per-child push wrap: each child's _create runs with
|
|
423
499
|
child as current, so the child's reactive bindings
|
|
424
500
|
register on child._disposers, not parent's.
|
|
501
|
+
beforeMount() ← user hook; signals/state ready, DOM not yet in tree
|
|
502
|
+
effects created here auto-register on this component
|
|
425
503
|
_setup() ← post-creation effects (rare; most go in _init)
|
|
426
504
|
mounted() ← user hook; DOM is in the tree now
|
|
427
505
|
__popComponent
|
|
@@ -436,6 +514,20 @@ unmount({ removeDOM = true }) ← idempotent (_unmounted flag short-circuits se
|
|
|
436
514
|
DOM removal (if requested)
|
|
437
515
|
```
|
|
438
516
|
|
|
517
|
+
### User hooks
|
|
518
|
+
|
|
519
|
+
The framework recognizes these hook names on any component. All are
|
|
520
|
+
optional; the runtime calls each only if defined.
|
|
521
|
+
|
|
522
|
+
| Hook | When it fires | Notes |
|
|
523
|
+
| --------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
524
|
+
| `beforeMount` | After `_create`, before DOM is attached | Effects created here auto-register on the component |
|
|
525
|
+
| `mounted` | After DOM attached | Runs once per visit |
|
|
526
|
+
| `beforeUnmount` | Before children unmount and disposers fire | Signals/effects still live |
|
|
527
|
+
| `unmounted` | After disposers fire and DOM is removed | Final notification; runs once per visit |
|
|
528
|
+
| `onError` | A throw escapes any component method (render, hook, event) | Receives `{ status?, message?, error?, path? }`; the renderer walks the layout chain to find the nearest defining component |
|
|
529
|
+
|
|
530
|
+
|
|
439
531
|
### Effect ownership
|
|
440
532
|
|
|
441
533
|
Every `__effect(fn)` call automatically registers its disposer with
|
package/docs/RIP-LANG.md
CHANGED
|
@@ -1593,12 +1593,11 @@ App = component
|
|
|
1593
1593
|
|
|
1594
1594
|
```coffee
|
|
1595
1595
|
App = component
|
|
1596
|
-
beforeMount
|
|
1597
|
-
mounted
|
|
1598
|
-
updated = -> p "updated"
|
|
1596
|
+
beforeMount = -> p "about to mount"
|
|
1597
|
+
mounted = -> p "mounted"
|
|
1599
1598
|
beforeUnmount = -> p "about to unmount"
|
|
1600
|
-
unmounted
|
|
1601
|
-
onError
|
|
1599
|
+
unmounted = -> p "unmounted"
|
|
1600
|
+
onError = (err) -> p "caught: #{err.message}"
|
|
1602
1601
|
```
|
|
1603
1602
|
|
|
1604
1603
|
**Effects:**
|