rip-lang 3.16.1 → 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 +2 -3
- package/bin/rip +39 -8
- package/bin/rip-schema +175 -0
- package/docs/RIP-APP.md +91 -2
- package/docs/RIP-DUCKDB.md +64 -1
- package/docs/RIP-INTRO.md +4 -4
- package/docs/RIP-LANG.md +32 -33
- package/docs/RIP-SCHEMA.md +1204 -364
- package/docs/dist/rip.js +3245 -611
- package/docs/dist/rip.min.js +1161 -289
- package/docs/dist/rip.min.js.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-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/hljs-rip.js +1 -1
- package/package.json +7 -4
- package/src/AGENTS.md +39 -8
- package/src/compiler.js +220 -36
- package/src/components.js +315 -14
- package/src/dts.js +18 -3
- 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 +24 -17
- 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 +1049 -89
- package/src/typecheck.js +283 -55
- package/src/types.js +5 -1
- package/src/grammar/lunar.rip +0 -2412
package/src/typecheck.js
CHANGED
|
@@ -132,6 +132,110 @@ export function findStashFile(filePath) {
|
|
|
132
132
|
return result;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// ── Static gate-path validation ────────────────────────────────────
|
|
136
|
+
//
|
|
137
|
+
// `x <~ @app.data.path` requires the path to resolve to a source key —
|
|
138
|
+
// the renderer enforces this with a deterministic mount-time error, and
|
|
139
|
+
// `rip check` mirrors it at compile time by reading the stash module's
|
|
140
|
+
// s-expressions: collect module-level bindings, find the `stash` object
|
|
141
|
+
// literal, and classify each gate path's keys. The analysis is
|
|
142
|
+
// CONSERVATIVE: only paths that provably land on a plain key (or on no
|
|
143
|
+
// key) error; anything whose source-ness isn't statically visible —
|
|
144
|
+
// imported cells, factory calls, spreads — stays silent and the mount
|
|
145
|
+
// check backstops it. False positives are the failure mode to avoid;
|
|
146
|
+
// false negatives just fall back to today's runtime error.
|
|
147
|
+
|
|
148
|
+
function sexprName(v) {
|
|
149
|
+
return (typeof v === 'string' || v instanceof String) ? v.valueOf() : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Walk a stash module's program s-expression into { stashObj, bindings }:
|
|
153
|
+
// the `stash` object literal plus every module-level `name = value`
|
|
154
|
+
// binding (so `orders: ordersSource` can chase `ordersSource = source …`).
|
|
155
|
+
export function collectStashAnalysis(sexpr) {
|
|
156
|
+
if (!Array.isArray(sexpr) || sexpr[0] !== 'program') return null;
|
|
157
|
+
const bindings = new Map();
|
|
158
|
+
for (let stmt of sexpr.slice(1)) {
|
|
159
|
+
if (!Array.isArray(stmt)) continue;
|
|
160
|
+
if (stmt[0] === 'export' && Array.isArray(stmt[1])) stmt = stmt[1];
|
|
161
|
+
if (stmt[0] === '=' || stmt[0] === 'readonly') {
|
|
162
|
+
const name = sexprName(stmt[1]);
|
|
163
|
+
if (name) bindings.set(name, stmt[2]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const stashObj = bindings.get('stash');
|
|
167
|
+
if (!Array.isArray(stashObj) || sexprName(stashObj[0]) !== 'object') return null;
|
|
168
|
+
return { stashObj, bindings };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 'source' | 'plain' | 'unknown' | { kind: 'object', node } — what a stash
|
|
172
|
+
// value provably is. Identifier references chase module-level bindings.
|
|
173
|
+
function classifyStashValue(node, bindings, depth = 0) {
|
|
174
|
+
if (depth > 8) return 'unknown';
|
|
175
|
+
const name = sexprName(node);
|
|
176
|
+
if (name != null) {
|
|
177
|
+
if (/^["'\d]/.test(name) || name === 'true' || name === 'false' ||
|
|
178
|
+
name === 'null' || name === 'undefined') return 'plain';
|
|
179
|
+
if (bindings.has(name)) return classifyStashValue(bindings.get(name), bindings, depth + 1);
|
|
180
|
+
return 'unknown'; // imported or otherwise invisible
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(node)) return 'unknown';
|
|
183
|
+
const head = sexprName(node[0]);
|
|
184
|
+
if (head === 'source') return 'source'; // application of the source() declarator
|
|
185
|
+
if (head === 'object') return { kind: 'object', node };
|
|
186
|
+
if (head === 'array') return 'plain';
|
|
187
|
+
return 'unknown';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Shared by `rip check` and the LSP: turn a file's hoisted gates into
|
|
191
|
+
// diagnostic records against the stash analysis. Lines/cols are 1-based
|
|
192
|
+
// (the LSP converts). Only provably-wrong paths produce records — see
|
|
193
|
+
// validateGatePath below for the conservative verdict rules.
|
|
194
|
+
export function collectGateDiagnostics(gates, source, analysis) {
|
|
195
|
+
const out = [];
|
|
196
|
+
if (!gates || !gates.length || !analysis) return out;
|
|
197
|
+
const srcLines = source.split('\n');
|
|
198
|
+
for (const g of gates) {
|
|
199
|
+
const verdict = validateGatePath(g.path, analysis);
|
|
200
|
+
if (verdict !== 'plain' && verdict !== 'missing') continue;
|
|
201
|
+
const lineText = srcLines[g.line - 1] ?? '';
|
|
202
|
+
const idx = lineText.indexOf('@app.data');
|
|
203
|
+
const detail = verdict === 'missing'
|
|
204
|
+
? `'${g.path}' is not a key of the stash`
|
|
205
|
+
: `'${g.path}' is a plain key`;
|
|
206
|
+
out.push({
|
|
207
|
+
line: g.line,
|
|
208
|
+
col: (idx >= 0 ? idx : 0) + 1,
|
|
209
|
+
len: idx >= 0 ? '@app.data.'.length + String(g.path).length : Math.max(lineText.trim().length, 1),
|
|
210
|
+
message: `'${g.path} <~' does not resolve to a source (${detail}) — declare it with source() in app/stash.rip`,
|
|
211
|
+
srcLine: lineText,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Verdict for one gate path against the stash literal:
|
|
218
|
+
// 'source' (ok), 'unknown' (silent), 'plain' / 'missing' (error).
|
|
219
|
+
// Walks segments through plain object literals; stops at the nearest
|
|
220
|
+
// source on the path (subpath gates bind under the loaded value).
|
|
221
|
+
export function validateGatePath(path, analysis) {
|
|
222
|
+
let node = analysis.stashObj;
|
|
223
|
+
for (const seg of String(path).split('.')) {
|
|
224
|
+
const entries = new Map();
|
|
225
|
+
let hasOpaqueEntries = false;
|
|
226
|
+
for (const e of node.slice(1)) {
|
|
227
|
+
const key = Array.isArray(e) && sexprName(e[0]) === ':' ? sexprName(e[1]) : null;
|
|
228
|
+
if (key != null) entries.set(key, e[2]);
|
|
229
|
+
else hasOpaqueEntries = true; // spread / computed key — can't enumerate
|
|
230
|
+
}
|
|
231
|
+
if (!entries.has(seg)) return hasOpaqueEntries ? 'unknown' : 'missing';
|
|
232
|
+
const c = classifyStashValue(entries.get(seg), analysis.bindings);
|
|
233
|
+
if (c === 'source' || c === 'unknown' || c === 'plain') return c;
|
|
234
|
+
node = c.node; // plain object literal — walk deeper
|
|
235
|
+
}
|
|
236
|
+
return 'plain'; // path exhausted inside plain literals, no source found
|
|
237
|
+
}
|
|
238
|
+
|
|
135
239
|
// ── Route tree discovery ───────────────────────────────────────────
|
|
136
240
|
//
|
|
137
241
|
// The project's routes live under `<projectRoot>/app/routes/` — a fixed
|
|
@@ -192,7 +296,10 @@ export function walkRoutesDir(routesDir) {
|
|
|
192
296
|
// Skip _-prefixed files (_layout.rip etc.) and dirs (shared
|
|
193
297
|
// helpers, not pages). Same rule as runtime buildRoutes.
|
|
194
298
|
if (e.name.startsWith('_')) continue;
|
|
195
|
-
|
|
299
|
+
// Pathless route groups: a `(name)` directory organizes routes and
|
|
300
|
+
// shares a _layout.rip without contributing a URL segment. Recurse
|
|
301
|
+
// but don't add it to the path. Mirrors runtime fileToPattern.
|
|
302
|
+
if (e.isDirectory()) walk(resolve(dir, e.name), /^\(.+\)$/.test(e.name) ? segs : [...segs, e.name]);
|
|
196
303
|
else if (e.isFile() && e.name.endsWith('.rip')) {
|
|
197
304
|
const base = e.name.slice(0, -'.rip'.length);
|
|
198
305
|
const fileSegs = base === 'index' ? segs : [...segs, base];
|
|
@@ -927,26 +1034,36 @@ export function createTypeCheckSettings(ts, overrides = {}) {
|
|
|
927
1034
|
// sub-package check would miss workspace-root ambients. Accepts symlinks
|
|
928
1035
|
// because bun's nested-package layout symlinks `@types/bun` to
|
|
929
1036
|
// `.bun/@types+bun@.../node_modules/@types/bun`.
|
|
1037
|
+
//
|
|
1038
|
+
// rip targets the Bun runtime, so `Bun`, `process`, `Buffer`, etc. are
|
|
1039
|
+
// always present — rip ships `@types/bun` as a dependency and includes
|
|
1040
|
+
// its own `node_modules/@types` last, so every project type-checks those
|
|
1041
|
+
// globals without installing anything. A workspace that installs its own
|
|
1042
|
+
// `@types/bun`/`@types/node` still wins: its dir is walked first and the
|
|
1043
|
+
// name dedup below keeps that copy.
|
|
930
1044
|
export function collectAmbientTypes(rootPath) {
|
|
931
1045
|
const typeRoots = [];
|
|
932
1046
|
const types = [];
|
|
1047
|
+
const scan = (cand) => {
|
|
1048
|
+
if (typeRoots.includes(cand) || !existsSync(cand)) return;
|
|
1049
|
+
typeRoots.push(cand);
|
|
1050
|
+
try {
|
|
1051
|
+
for (const entry of readdirSync(cand, { withFileTypes: true })) {
|
|
1052
|
+
if ((entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith('.') && !types.includes(entry.name)) {
|
|
1053
|
+
types.push(entry.name);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
} catch {}
|
|
1057
|
+
};
|
|
933
1058
|
let dir = rootPath;
|
|
934
1059
|
while (true) {
|
|
935
|
-
|
|
936
|
-
if (existsSync(cand)) {
|
|
937
|
-
typeRoots.push(cand);
|
|
938
|
-
try {
|
|
939
|
-
for (const entry of readdirSync(cand, { withFileTypes: true })) {
|
|
940
|
-
if ((entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith('.') && !types.includes(entry.name)) {
|
|
941
|
-
types.push(entry.name);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
} catch {}
|
|
945
|
-
}
|
|
1060
|
+
scan(resolve(dir, 'node_modules/@types'));
|
|
946
1061
|
const parent = dirname(dir);
|
|
947
1062
|
if (parent === dir) break;
|
|
948
1063
|
dir = parent;
|
|
949
1064
|
}
|
|
1065
|
+
// rip's own bundled ambients (this file lives at <rip-lang>/src/typecheck.js).
|
|
1066
|
+
scan(resolve(import.meta.dirname, '../node_modules/@types'));
|
|
950
1067
|
return { typeRoots, types };
|
|
951
1068
|
}
|
|
952
1069
|
|
|
@@ -1149,17 +1266,55 @@ function injectTypeParams(line, typeParams) {
|
|
|
1149
1266
|
export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
1150
1267
|
const result = compiler.compile(source, { sourceMap: true, types: 'emit', skipPreamble: true, stubComponents: true, inlineTypes: true });
|
|
1151
1268
|
let code = result.code || '';
|
|
1152
|
-
|
|
1269
|
+
let dts = result.dts ? result.dts.trimEnd() + '\n' : '';
|
|
1270
|
+
|
|
1271
|
+
// ── Schema shadow reconciliation ──────────────────────────────────────
|
|
1272
|
+
// The compiled body keeps its runtime `const Name = __schema({...})`
|
|
1273
|
+
// bindings verbatim, so source maps stay exact. But in a `.ts` shadow that
|
|
1274
|
+
// body references an undeclared `__schema` (skipPreamble drops the runtime
|
|
1275
|
+
// import) and would also collide with the dts `declare const Name`. Rewrite
|
|
1276
|
+
// each schema `const` declaration into a `__schema` overload keyed on the
|
|
1277
|
+
// schema's `name` literal, so the body's `__schema({name:"Name",...})` call
|
|
1278
|
+
// resolves to the precise Schema/ModelSchema type with no duplicate binding.
|
|
1279
|
+
// The `type` aliases (NameValue/NameData/NameInstance) are kept untouched —
|
|
1280
|
+
// importers and the body both reference them.
|
|
1281
|
+
let usesSchemas = false;
|
|
1282
|
+
if (dts) {
|
|
1283
|
+
const overloads = [];
|
|
1284
|
+
const kept = [];
|
|
1285
|
+
for (const line of dts.split('\n')) {
|
|
1286
|
+
const m = line.match(/^(?:export )?declare const (\w+): (.+);\s*$/);
|
|
1287
|
+
if (m && /^(?:Schema<|ModelSchema<|\{ parse\()/.test(m[2])) {
|
|
1288
|
+
usesSchemas = true;
|
|
1289
|
+
overloads.push(`declare function __schema(d: { name: "${m[1]}"; [k: string]: any }): ${m[2]};`);
|
|
1290
|
+
} else {
|
|
1291
|
+
// Anonymous-schema overloads are emitted directly by the dts pass
|
|
1292
|
+
// (keyed on the descriptor's `__anon` marker); keep them, but they
|
|
1293
|
+
// still mark the file as schema-using so the `(d: any) => any`
|
|
1294
|
+
// fallback and registry declares are appended.
|
|
1295
|
+
if (/^declare function __schema\(/.test(line)) usesSchemas = true;
|
|
1296
|
+
kept.push(line);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (usesSchemas) {
|
|
1300
|
+
overloads.push('declare function __schema(d: any): any;');
|
|
1301
|
+
overloads.push('declare const SchemaError: any;');
|
|
1302
|
+
overloads.push('declare const __SchemaRegistry: any;');
|
|
1303
|
+
overloads.push('declare const __schemaSetAdapter: any;');
|
|
1304
|
+
dts = kept.join('\n').trimEnd() + '\n' + overloads.join('\n') + '\n';
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1153
1307
|
|
|
1154
1308
|
// Determine if this file should be type-checked.
|
|
1155
1309
|
// A `# @nocheck` comment near the top of the file opts out entirely.
|
|
1156
1310
|
// In strict mode, all non-nocheck files are type-checked.
|
|
1157
1311
|
const nocheck = /^#\s*@nocheck\b/m.test(source.slice(0, NOCHECK_SCAN_LIMIT));
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1312
|
+
// A file that declares schemas has an exportable type surface (its
|
|
1313
|
+
// `NameValue`/`NameInstance` aliases), so it must be checked even without
|
|
1314
|
+
// explicit `::`/`type` annotations — otherwise importers can't resolve those
|
|
1315
|
+
// names. `usesSchemas` is derived from the compiled dts, not a raw-source
|
|
1316
|
+
// regex, so it never false-fires on `schema` inside heredoc literals.
|
|
1317
|
+
const hasOwnTypes = !nocheck && (hasTypeAnnotations(source) || !!opts.checkAll || usesSchemas);
|
|
1163
1318
|
let importsTyped = false;
|
|
1164
1319
|
if (!hasOwnTypes && !nocheck) {
|
|
1165
1320
|
const ripImports = [...source.matchAll(/from\s+['"]([^'"]*\.rip)['"]/g)];
|
|
@@ -1763,6 +1918,13 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
|
1763
1918
|
if (needEffect) decls.push(EFFECT_FN);
|
|
1764
1919
|
headerDts = decls.join('\n') + '\n' + headerDts;
|
|
1765
1920
|
}
|
|
1921
|
+
// Gated bindings (`x <~ @app.data.x`) stub as
|
|
1922
|
+
// `__computed(() => __ripGate(this.app.data.x))`. __ripGate is the
|
|
1923
|
+
// generic narrow — soundness is supplied by the runtime gate, which
|
|
1924
|
+
// loads the source before the component is constructed.
|
|
1925
|
+
if (/\b__ripGate\(/.test(code) && !/\bdeclare function __ripGate\b/.test(headerDts)) {
|
|
1926
|
+
headerDts = 'declare function __ripGate<T>(v: T | null | undefined): T;\n' + headerDts;
|
|
1927
|
+
}
|
|
1766
1928
|
}
|
|
1767
1929
|
|
|
1768
1930
|
// Inject declarations for Rip's stdlib globals (abort, assert, p, sleep, etc.)
|
|
@@ -2022,19 +2184,28 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
|
2022
2184
|
const isStash = stashFile && stashFile === filePath;
|
|
2023
2185
|
|
|
2024
2186
|
if (isStash) {
|
|
2025
|
-
//
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
//
|
|
2031
|
-
//
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2187
|
+
// For an ANNOTATED stash (`stash:: T = ...`) the DTS header hoists
|
|
2188
|
+
// `export let stash: <Type>;` so the type is visible everywhere. The
|
|
2189
|
+
// body emits either `let stash; ... stash = {...}` (no export) or
|
|
2190
|
+
// `export const stash = {...}` (with export). Both conflict with the
|
|
2191
|
+
// typed hoist — TS sees a redeclaration and the un-annotated body
|
|
2192
|
+
// wins, collapsing the inferred type to `{ items: never[], ... }`.
|
|
2193
|
+
// Rewrite both forms into a bare assignment to the already-declared
|
|
2194
|
+
// `stash`, preserving the contextual type.
|
|
2195
|
+
//
|
|
2196
|
+
// An UNANNOTATED stash (`export stash = ...`, the inference path —
|
|
2197
|
+
// source() keys carry their own types) hoists nothing, so the
|
|
2198
|
+
// body declaration must stay: removing it left `typeof stash` with no
|
|
2199
|
+
// `stash` at all.
|
|
2200
|
+
const stashHoisted = /(^|\n)\s*export\s+(let|const|var)\s+stash\s*:/.test(headerDts || '');
|
|
2201
|
+
if (stashHoisted) {
|
|
2202
|
+
const letRe = /^(\s*let\s+)([^;=]+);/m;
|
|
2203
|
+
code = code.replace(letRe, (full, prefix, names) => {
|
|
2204
|
+
const remaining = names.split(',').map(s => s.trim()).filter(n => n !== 'stash');
|
|
2205
|
+
return remaining.length ? `${prefix}${remaining.join(', ')};` : '';
|
|
2206
|
+
});
|
|
2207
|
+
code = code.replace(/^(\s*)export\s+const\s+stash\s*=/m, '$1stash =');
|
|
2208
|
+
}
|
|
2038
2209
|
code += `\nexport type __RipStash = typeof stash;\n`;
|
|
2039
2210
|
}
|
|
2040
2211
|
|
|
@@ -2042,7 +2213,10 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
|
2042
2213
|
let typedApp = null;
|
|
2043
2214
|
if (stashFile && !isStash) {
|
|
2044
2215
|
const spec = entryImportSpec(filePath, stashFile);
|
|
2045
|
-
|
|
2216
|
+
// data intersects the stash shape with the reserved stash methods
|
|
2217
|
+
// (inc/dec/…, peek/reset, and the source handle) so they carry
|
|
2218
|
+
// signatures and completion in typed projects.
|
|
2219
|
+
typedApp = `declare app: { data: import('${spec}').__RipStash & import('@rip-lang/app').StashMethods<import('${spec}').__RipStash>; components: any; routes: any; params: any; query: any; router: any }`;
|
|
2046
2220
|
}
|
|
2047
2221
|
if (typedApp) code = code.replace(/declare app: any/g, typedApp);
|
|
2048
2222
|
}
|
|
@@ -2150,16 +2324,14 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
|
2150
2324
|
);
|
|
2151
2325
|
}
|
|
2152
2326
|
|
|
2153
|
-
// (b)
|
|
2154
|
-
//
|
|
2155
|
-
//
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
2158
|
-
//
|
|
2159
|
-
// overloaded methods makes the parameter type a union
|
|
2160
|
-
// (contravariance), which loses the narrowing.
|
|
2327
|
+
// (b) Route-check router.push / router.replace the same way as <a href>:
|
|
2328
|
+
// slash-prefixed string LITERALS must be a known route (typos caught, clean
|
|
2329
|
+
// route-list errors); dynamic strings and external URLs fall through. Build
|
|
2330
|
+
// query/hash URLs as `string` values rather than slash-prefixed literals.
|
|
2331
|
+
// Omit + re-add instead of intersection: intersecting overloaded methods
|
|
2332
|
+
// unions the parameter type (contravariance), which loses the narrowing.
|
|
2161
2333
|
if (code.includes(`declare router: import('@rip-lang/app').Router`)) {
|
|
2162
|
-
const typedRouter = `declare router: Omit<import('@rip-lang/app').Router, 'push'> & { push(url: ${inlineRoutesUnion}, opts?: NavOpts): void; }`;
|
|
2334
|
+
const typedRouter = `declare router: Omit<import('@rip-lang/app').Router, 'push' | 'replace'> & { push<const P extends string>(url: P extends \`/\${string}\` ? ${inlineRoutesUnion} : P, opts?: NavOpts): void; replace<const U extends string>(url: U extends \`/\${string}\` ? ${inlineRoutesUnion} : U, opts?: NavOpts): void; }`;
|
|
2163
2335
|
code = code.replace(
|
|
2164
2336
|
/declare router: import\('@rip-lang\/app'\)\.Router(?![ &])/g,
|
|
2165
2337
|
typedRouter,
|
|
@@ -2360,7 +2532,7 @@ export function compileForCheck(filePath, source, compiler, opts = {}) {
|
|
|
2360
2532
|
}
|
|
2361
2533
|
}
|
|
2362
2534
|
|
|
2363
|
-
return { tsContent, headerLines, hasTypes, srcToGen, genToSrc, srcColToGen, source, dts };
|
|
2535
|
+
return { tsContent, headerLines, hasTypes, srcToGen, genToSrc, srcColToGen, source, dts, sexpr: result.sexpr, gates: result.gates || [] };
|
|
2364
2536
|
}
|
|
2365
2537
|
|
|
2366
2538
|
// ── Source-position mapping helpers ────────────────────────────────
|
|
@@ -2948,6 +3120,38 @@ export function retargetObjectArgOffset(entry, srcLine, srcCol, genOffset) {
|
|
|
2948
3120
|
return genOffset - lc.col + i;
|
|
2949
3121
|
}
|
|
2950
3122
|
|
|
3123
|
+
// Completion-offset fixup for object-literal property *values* whose owning
|
|
3124
|
+
// object spans several source lines that collapse to a single generated line.
|
|
3125
|
+
// An implicit-object call block —
|
|
3126
|
+
// use serve
|
|
3127
|
+
// preload: '▮'
|
|
3128
|
+
// — compiles to `use(serve({ …, preload: '' }))`, so srcToOffset maps the
|
|
3129
|
+
// property's source line onto that one statement gen line but lands near the
|
|
3130
|
+
// statement start, not inside the value string. Given the source line, the
|
|
3131
|
+
// cursor column, and the gen offset srcToOffset produced, this finds the same
|
|
3132
|
+
// `key: <quote>` on the gen line and returns the absolute gen offset at the
|
|
3133
|
+
// matching spot inside the gen string — so TS can supply the contextually-typed
|
|
3134
|
+
// string-literal completions. Returns genOffset unchanged outside this context.
|
|
3135
|
+
export function retargetObjectValueOffset(entry, srcLine, srcCol, genOffset) {
|
|
3136
|
+
if (!entry || genOffset == null || !srcLine) return genOffset;
|
|
3137
|
+
const m = srcLine.match(/^(\s*)([\w$]+)(\s*:\s*)(["'])/);
|
|
3138
|
+
if (!m) return genOffset;
|
|
3139
|
+
const key = m[2];
|
|
3140
|
+
const strStart = m[0].length; // src index just inside the opening quote
|
|
3141
|
+
if (srcCol < strStart) return genOffset; // cursor isn't inside the value string
|
|
3142
|
+
const inStr = srcCol - strStart; // characters into the string content
|
|
3143
|
+
|
|
3144
|
+
const tsText = entry.tsContent;
|
|
3145
|
+
const lc = offsetToLineCol(tsText, genOffset);
|
|
3146
|
+
const genLine = tsText.split('\n')[lc.line];
|
|
3147
|
+
if (genLine == null) return genOffset;
|
|
3148
|
+
const re = new RegExp('\\b' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*:\\s*["\']');
|
|
3149
|
+
const gm = re.exec(genLine);
|
|
3150
|
+
if (!gm) return genOffset;
|
|
3151
|
+
const genStrInside = gm.index + gm[0].length; // just inside the gen opening quote
|
|
3152
|
+
return (genOffset - lc.col) + genStrInside + inStr;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
2951
3155
|
// Map a Rip source (line, col) to a TypeScript virtual file byte offset.
|
|
2952
3156
|
// This is the forward direction: source → generated (used for hover, definition, etc.)
|
|
2953
3157
|
//
|
|
@@ -3250,15 +3454,16 @@ const bold = (s) => isColor ? `\x1b[1m${s}\x1b[0m` : s;
|
|
|
3250
3454
|
export async function runCheck(targetDir, opts = {}) {
|
|
3251
3455
|
const rootPath = resolve(targetDir);
|
|
3252
3456
|
|
|
3457
|
+
// Use rip's own catalog-pinned TypeScript (a dependency), never the
|
|
3458
|
+
// consumer's — so `rip check` and the editor type-check with the exact
|
|
3459
|
+
// same version. import('typescript') resolves from this file's location
|
|
3460
|
+
// (rip-lang's node_modules) regardless of the consumer's setup.
|
|
3253
3461
|
let ts;
|
|
3254
3462
|
try {
|
|
3255
|
-
|
|
3256
|
-
ts = req('typescript');
|
|
3463
|
+
ts = await import('typescript').then(m => m.default || m);
|
|
3257
3464
|
} catch {
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
return 1;
|
|
3261
|
-
}
|
|
3465
|
+
console.error('TypeScript could not be loaded. Reinstall rip-lang (it ships TypeScript as a dependency).');
|
|
3466
|
+
return 1;
|
|
3262
3467
|
}
|
|
3263
3468
|
|
|
3264
3469
|
if (!existsSync(rootPath)) {
|
|
@@ -3477,7 +3682,19 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
3477
3682
|
}
|
|
3478
3683
|
}
|
|
3479
3684
|
|
|
3480
|
-
// Check for unresolved relative imports in all files (not just typed ones)
|
|
3685
|
+
// Check for unresolved relative imports in all files (not just typed ones),
|
|
3686
|
+
// and validate render gates (<~) against the stash's source-key set.
|
|
3687
|
+
const stashAnalyses = new Map();
|
|
3688
|
+
const stashAnalysisFor = (fp) => {
|
|
3689
|
+
const stashFile = findStashFile(fp);
|
|
3690
|
+
if (!stashFile) return null;
|
|
3691
|
+
if (!stashAnalyses.has(stashFile)) {
|
|
3692
|
+
const entry = compiled.get(stashFile);
|
|
3693
|
+
stashAnalyses.set(stashFile, entry?.sexpr ? collectStashAnalysis(entry.sexpr) : null);
|
|
3694
|
+
}
|
|
3695
|
+
return stashAnalyses.get(stashFile);
|
|
3696
|
+
};
|
|
3697
|
+
|
|
3481
3698
|
const fileResults = [];
|
|
3482
3699
|
let totalErrors = 0, totalWarnings = 0;
|
|
3483
3700
|
for (const [fp, source] of sourcesByPath) {
|
|
@@ -3494,6 +3711,18 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
3494
3711
|
totalErrors++;
|
|
3495
3712
|
}
|
|
3496
3713
|
}
|
|
3714
|
+
|
|
3715
|
+
// A gate whose path provably lands on a plain key (or no key)
|
|
3716
|
+
// in app/stash.rip is the compile-time form of the renderer's
|
|
3717
|
+
// deterministic mount error. Conservative — see validateGatePath.
|
|
3718
|
+
const gates = compiled.get(fp)?.gates;
|
|
3719
|
+
if (gates && gates.length) {
|
|
3720
|
+
for (const d of collectGateDiagnostics(gates, source, stashAnalysisFor(fp))) {
|
|
3721
|
+
errors.push({ line: d.line, col: d.col, len: d.len, message: d.message, severity: 'error', code: 'rip', srcLine: d.srcLine, related: [] });
|
|
3722
|
+
totalErrors++;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3497
3726
|
if (errors.length > 0) fileResults.push({ file: fp, errors });
|
|
3498
3727
|
}
|
|
3499
3728
|
|
|
@@ -4202,15 +4431,14 @@ function joinPath(label, step, rest) {
|
|
|
4202
4431
|
export async function runAudit(targetDir) {
|
|
4203
4432
|
const rootPath = resolve(targetDir);
|
|
4204
4433
|
|
|
4434
|
+
// Use rip's own catalog-pinned TypeScript (a dependency), never the
|
|
4435
|
+
// consumer's — see runCheck above.
|
|
4205
4436
|
let ts;
|
|
4206
4437
|
try {
|
|
4207
|
-
|
|
4208
|
-
ts = req('typescript');
|
|
4438
|
+
ts = await import('typescript').then(m => m.default || m);
|
|
4209
4439
|
} catch {
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
return 1;
|
|
4213
|
-
}
|
|
4440
|
+
console.error('TypeScript could not be loaded. Reinstall rip-lang (it ships TypeScript as a dependency).');
|
|
4441
|
+
return 1;
|
|
4214
4442
|
}
|
|
4215
4443
|
|
|
4216
4444
|
const pkgJsonPath = resolve(rootPath, 'package.json');
|
package/src/types.js
CHANGED
|
@@ -378,7 +378,7 @@ function collectTypeExpression(tokens, j) {
|
|
|
378
378
|
}
|
|
379
379
|
if (tTag === '=' || tTag === 'REACTIVE_ASSIGN' ||
|
|
380
380
|
tTag === 'COMPUTED_ASSIGN' || tTag === 'READONLY_ASSIGN' ||
|
|
381
|
-
tTag === 'EFFECT' || tTag === 'TERMINATOR' ||
|
|
381
|
+
tTag === 'EFFECT' || tTag === 'GATE' || tTag === 'TERMINATOR' ||
|
|
382
382
|
tTag === 'INDENT' || tTag === 'OUTDENT' ||
|
|
383
383
|
tTag === '->' || tTag === ',') {
|
|
384
384
|
break;
|
|
@@ -470,6 +470,10 @@ function buildTypeString(typeTokens) {
|
|
|
470
470
|
if (t.data?.optional && next && (next[0] === 'TYPE_ANNOTATION' || next[0] === ':')) {
|
|
471
471
|
return `${t[1]}?`;
|
|
472
472
|
}
|
|
473
|
+
// Embedded-JS tokens are backtick literals with the delimiters stripped
|
|
474
|
+
// by the lexer. In type position they are TS template-literal types
|
|
475
|
+
// (e.g. `${number} min`) — re-wrap them so the emitted type is valid.
|
|
476
|
+
if (t[0] === 'JS') return '`' + t[1] + '`';
|
|
473
477
|
return t[1];
|
|
474
478
|
});
|
|
475
479
|
let typeStr = parts.join(' ').replace(/\s+/g, ' ').trim();
|