rip-lang 3.14.5 → 3.15.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 +9 -5
- package/bin/rip +5 -0
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-LANG.md +17 -5
- package/docs/RIP-SCHEMA.md +4 -4
- package/docs/demo/README.md +43 -0
- package/docs/demo/components/_layout.rip +28 -0
- package/docs/demo/components/about.rip +36 -0
- package/docs/demo/components/card.rip +10 -0
- package/docs/demo/components/counter.rip +33 -0
- package/docs/demo/components/index.rip +30 -0
- package/docs/demo/components/todos.rip +48 -0
- package/docs/demo/css/styles.css +472 -0
- package/docs/dist/rip.js +3211 -4619
- package/docs/dist/rip.min.js +270 -683
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +6 -6
- package/docs/extensions/duckdb/index.html +7 -5
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/package.json +8 -3
- package/src/AGENTS.md +107 -9
- package/src/{ui.rip → app.rip} +24 -2
- package/src/browser.js +154 -37
- package/src/compiler.js +87 -9
- package/src/grammar/grammar.rip +1 -1
- package/src/grammar/solar.rip +0 -1
- package/src/lexer.js +25 -3
- package/src/parser.js +4 -4
- package/src/schema/dts-emit.js +329 -0
- package/src/schema/loader-browser.js +55 -0
- package/src/schema/loader-server.js +65 -0
- package/src/schema/runtime-browser-stubs.js +51 -0
- package/src/schema/runtime-db-naming.js +34 -0
- package/src/schema/runtime-ddl.js +124 -0
- package/src/schema/runtime-orm.js +294 -0
- package/src/schema/runtime-validate.js +816 -0
- package/src/schema/runtime.generated.js +1315 -0
- package/src/{schema.js → schema/schema.js} +43 -1627
- package/src/typecheck.js +3 -2
- package/src/types-emit.js +1021 -0
- package/src/types.js +11 -1035
package/src/compiler.js
CHANGED
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
import { Lexer } from './lexer.js';
|
|
12
12
|
import { parser } from './parser.js';
|
|
13
13
|
import { installComponentSupport } from './components.js';
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Type emission is CLI/editor-only. types-emit.js registers itself via
|
|
15
|
+
// setTypesEmitter() at module load. The browser never imports types-emit,
|
|
16
|
+
// so _typesEmitter stays null and .d.ts output is silently skipped.
|
|
17
|
+
let _typesEmitter = null;
|
|
18
|
+
export function setTypesEmitter(fn) { _typesEmitter = fn; }
|
|
19
|
+
import { installSchemaSupport } from './schema/schema.js';
|
|
16
20
|
import { SourceMapGenerator } from './sourcemaps.js';
|
|
17
21
|
import { RipError, toRipError } from './error.js';
|
|
18
22
|
|
|
@@ -2045,7 +2049,22 @@ export class CodeEmitter {
|
|
|
2045
2049
|
emitComprehension(head, rest, context) {
|
|
2046
2050
|
let [expr, iterators, guards] = rest;
|
|
2047
2051
|
if (context === 'statement') return this.emitComprehensionAsLoop(expr, iterators, guards);
|
|
2048
|
-
if (this.comprehensionTarget)
|
|
2052
|
+
if (this.comprehensionTarget) {
|
|
2053
|
+
// Consume-and-clear: the auto-return-loop logic sets comprehensionTarget
|
|
2054
|
+
// expecting ONE consumer (the direct value-context comprehension being
|
|
2055
|
+
// routed to the named target). Without clearing here, nested
|
|
2056
|
+
// comprehensions inside this comprehension's body (call args, RHS
|
|
2057
|
+
// expressions) would inherit the target and skip their own IIFE,
|
|
2058
|
+
// producing malformed JS or wrong semantics. The body's own emit calls
|
|
2059
|
+
// see comprehensionTarget = null and correctly produce IIFEs.
|
|
2060
|
+
let target = this.comprehensionTarget;
|
|
2061
|
+
this.comprehensionTarget = null;
|
|
2062
|
+
try {
|
|
2063
|
+
return this.emitComprehensionWithTarget(expr, iterators, guards, target);
|
|
2064
|
+
} finally {
|
|
2065
|
+
this.comprehensionTarget = target;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2049
2068
|
|
|
2050
2069
|
// Enclosed: expr, iterators (iterable expressions), guards
|
|
2051
2070
|
let hasAwait = this.containsAwait(expr) || iterators.some(i => this.containsAwait(i)) || guards.some(g => this.containsAwait(g));
|
|
@@ -2614,6 +2633,21 @@ export class CodeEmitter {
|
|
|
2614
2633
|
code += this.indent() + this.addSemicolon(stmt, this.emit(stmt, 'statement')) + '\n';
|
|
2615
2634
|
return;
|
|
2616
2635
|
}
|
|
2636
|
+
// Auto-return-loop: only for-in/for-of/for-as auto-collect into
|
|
2637
|
+
// _result, because emitForIn/emitForOf/emitForAs at value-context
|
|
2638
|
+
// wrap themselves in a comprehension that consumes
|
|
2639
|
+
// comprehensionTarget. `loop` and `while` have no such wrapping —
|
|
2640
|
+
// emitLoop/emitWhile just emit `while(...) { body }` and any
|
|
2641
|
+
// comprehensionTarget set here would LEAK into nested
|
|
2642
|
+
// expression-context comprehensions inside the body (causing
|
|
2643
|
+
// them to be routed to the wrong target and skip their own
|
|
2644
|
+
// IIFE). Loops with explicit `return X` inside their body
|
|
2645
|
+
// already work correctly; emit them as plain statements.
|
|
2646
|
+
let isCollectibleLoop = h === 'for-in' || h === 'for-of' || h === 'for-as';
|
|
2647
|
+
if (!isCollectibleLoop) {
|
|
2648
|
+
code += this.indent() + this.addSemicolon(stmt, this.emit(stmt, 'statement')) + '\n';
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2617
2651
|
code += this.indent() + 'const _result = [];\n';
|
|
2618
2652
|
this.comprehensionTarget = '_result';
|
|
2619
2653
|
let saved = this._skipCompTargetInit;
|
|
@@ -3689,7 +3723,7 @@ export class Compiler {
|
|
|
3689
3723
|
|
|
3690
3724
|
// If only terminators remain (type-only source), emit types and return early
|
|
3691
3725
|
if (tokens.every(t => t[0] === 'TERMINATOR')) {
|
|
3692
|
-
if (typeTokens) dts =
|
|
3726
|
+
if (typeTokens && _typesEmitter) dts = _typesEmitter(typeTokens, ['program'], source);
|
|
3693
3727
|
return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {} };
|
|
3694
3728
|
}
|
|
3695
3729
|
|
|
@@ -3759,6 +3793,11 @@ export class Compiler {
|
|
|
3759
3793
|
skipDataPart: this.options.skipDataPart,
|
|
3760
3794
|
stubComponents: this.options.stubComponents,
|
|
3761
3795
|
reactiveVars: this.options.reactiveVars,
|
|
3796
|
+
// Schema runtime mode: 'browser' / 'validate' / 'server' / 'migration'.
|
|
3797
|
+
// Default 'migration' covers the common case (CLI, server, tests) where
|
|
3798
|
+
// the user might call any schema feature including .toSQL(). The browser
|
|
3799
|
+
// bundle build script overrides to 'browser' for size reduction.
|
|
3800
|
+
schemaMode: this.options.schemaMode,
|
|
3762
3801
|
sourceMap,
|
|
3763
3802
|
});
|
|
3764
3803
|
let code = generator.compile(sexpr);
|
|
@@ -3766,15 +3805,28 @@ export class Compiler {
|
|
|
3766
3805
|
let map = sourceMap ? sourceMap.toJSON() : null;
|
|
3767
3806
|
let reverseMap = sourceMap ? sourceMap.toReverseMap() : null;
|
|
3768
3807
|
if (map && this.options.sourceMap === 'inline') {
|
|
3769
|
-
|
|
3808
|
+
// map is already a JSON string (sourceMaps.toJSON() stringifies). UTF-8
|
|
3809
|
+
// safe encode: btoa() only handles Latin-1, so pre-encode non-ASCII via
|
|
3810
|
+
// TextEncoder before base64 in browsers. Bun's Buffer handles utf-8
|
|
3811
|
+
// directly. Source files containing emoji, em-dashes, accented chars,
|
|
3812
|
+
// etc. would otherwise break with `Failed to execute 'btoa'`.
|
|
3813
|
+
let b64;
|
|
3814
|
+
if (typeof Buffer !== 'undefined') {
|
|
3815
|
+
b64 = Buffer.from(map, 'utf8').toString('base64');
|
|
3816
|
+
} else {
|
|
3817
|
+
const bytes = new TextEncoder().encode(map);
|
|
3818
|
+
let bin = '';
|
|
3819
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
3820
|
+
b64 = btoa(bin);
|
|
3821
|
+
}
|
|
3770
3822
|
code += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
3771
3823
|
} else if (map && this.options.filename) {
|
|
3772
3824
|
code += `\n//# sourceMappingURL=${this.options.filename}.js.map`;
|
|
3773
3825
|
}
|
|
3774
3826
|
|
|
3775
3827
|
// Step 5: Emit .d.ts from annotated tokens + parsed s-expression
|
|
3776
|
-
if (typeTokens) {
|
|
3777
|
-
dts =
|
|
3828
|
+
if (typeTokens && _typesEmitter) {
|
|
3829
|
+
dts = _typesEmitter(typeTokens, sexpr, source);
|
|
3778
3830
|
}
|
|
3779
3831
|
|
|
3780
3832
|
return { tokens, sexpr, code, dts, map, reverseMap, data: dataSection, reactiveVars: generator.reactiveVars };
|
|
@@ -3791,10 +3843,36 @@ export class Compiler {
|
|
|
3791
3843
|
installComponentSupport(CodeEmitter, Lexer);
|
|
3792
3844
|
|
|
3793
3845
|
// =============================================================================
|
|
3794
|
-
//
|
|
3846
|
+
// Enum Codegen (CodeEmitter method)
|
|
3795
3847
|
// =============================================================================
|
|
3848
|
+
// `enum` blocks compile to a runtime JavaScript object that maps both
|
|
3849
|
+
// forward (key → value) and reverse (value → key). This is real codegen,
|
|
3850
|
+
// not type machinery, so it lives with the rest of the emitter dispatch.
|
|
3851
|
+
|
|
3852
|
+
CodeEmitter.prototype.emitEnum = function emitEnum(head, rest, context) {
|
|
3853
|
+
let [name, body] = rest;
|
|
3854
|
+
let enumName = name?.valueOf?.() ?? name;
|
|
3855
|
+
|
|
3856
|
+
let pairs = [];
|
|
3857
|
+
if (Array.isArray(body)) {
|
|
3858
|
+
let items = body[0] === 'block' ? body.slice(1) : [body];
|
|
3859
|
+
for (let item of items) {
|
|
3860
|
+
if (Array.isArray(item)) {
|
|
3861
|
+
if (item[0]?.valueOf?.() === '=') {
|
|
3862
|
+
let key = item[1]?.valueOf?.() ?? item[1];
|
|
3863
|
+
let val = item[2]?.valueOf?.() ?? item[2];
|
|
3864
|
+
pairs.push([key, val]);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
if (pairs.length === 0) return `const ${enumName} = {}`;
|
|
3796
3871
|
|
|
3797
|
-
|
|
3872
|
+
let forward = pairs.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
3873
|
+
let reverse = pairs.map(([k, v]) => `${v}: "${k}"`).join(', ');
|
|
3874
|
+
return `const ${enumName} = {${forward}, ${reverse}}`;
|
|
3875
|
+
};
|
|
3798
3876
|
|
|
3799
3877
|
// =============================================================================
|
|
3800
3878
|
// Schema Support (prototype installation)
|
package/src/grammar/grammar.rip
CHANGED
|
@@ -808,7 +808,7 @@ grammar =
|
|
|
808
808
|
# Schema
|
|
809
809
|
# ============================================================================
|
|
810
810
|
# `schema [:kind] INDENT ... OUTDENT` is parsed entirely by the schema
|
|
811
|
-
# sub-parser at lexer-rewrite time (src/schema.js). The rewriter emits
|
|
811
|
+
# sub-parser at lexer-rewrite time (src/schema/schema.js). The rewriter emits
|
|
812
812
|
# a synthetic SCHEMA_BODY token whose .data carries the full descriptor
|
|
813
813
|
# (kind, entries, per-entry loc). The main grammar only sees these two
|
|
814
814
|
# terminals, which keeps schema body syntax (`name! type`, `@directive`,
|
package/src/grammar/solar.rip
CHANGED
package/src/lexer.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// ==========================================================================
|
|
41
41
|
|
|
42
42
|
import { installTypeSupport } from './types.js';
|
|
43
|
-
import { installSchemaSupport } from './schema.js';
|
|
43
|
+
import { installSchemaSupport } from './schema/schema.js';
|
|
44
44
|
|
|
45
45
|
// ==========================================================================
|
|
46
46
|
// Token Category Sets
|
|
@@ -174,6 +174,15 @@ let TAGGABLE = new Set(['IDENTIFIER', 'PROPERTY', ')', 'CALL_END', ']', 'INDEX_E
|
|
|
174
174
|
// Control flow tokens that don't end implicit calls/objects
|
|
175
175
|
let CONTROL_IN_IMPLICIT = new Set(['IF', 'TRY', 'FINALLY', 'CATCH', 'CLASS', 'SWITCH', 'COMPONENT', 'FOR']);
|
|
176
176
|
|
|
177
|
+
// Tokens that complete an expression value. Used to detect postfix-position
|
|
178
|
+
// for keywords that exist in both prefix and postfix forms (FOR has no
|
|
179
|
+
// dedicated POST_FOR token like POST_IF, so we infer it from context).
|
|
180
|
+
let VALUE_END_TAGS = new Set([
|
|
181
|
+
'IDENTIFIER', 'PROPERTY', 'NUMBER', 'STRING', 'STRING_END', 'REGEX', 'REGEX_END',
|
|
182
|
+
')', 'CALL_END', ']', 'INDEX_END', '}', 'MAP_END', 'PICK_END',
|
|
183
|
+
'BOOL', 'NULL', 'UNDEFINED', 'INFINITY', 'NAN', 'SUPER', 'THIS', '@', 'SYMBOL',
|
|
184
|
+
]);
|
|
185
|
+
|
|
177
186
|
// Single-liner keywords that get implicit INDENT/OUTDENT
|
|
178
187
|
let SINGLE_LINERS = new Set(['ELSE', '->', '=>', 'TRY', 'FINALLY', 'THEN']);
|
|
179
188
|
|
|
@@ -1782,8 +1791,21 @@ export class Lexer {
|
|
|
1782
1791
|
i += 1;
|
|
1783
1792
|
};
|
|
1784
1793
|
|
|
1785
|
-
// Don't end implicit on INDENT for control flow inside implicit
|
|
1786
|
-
|
|
1794
|
+
// Don't end implicit on INDENT for control flow inside implicit.
|
|
1795
|
+
//
|
|
1796
|
+
// Special case: FOR is the only entry in CONTROL_IN_IMPLICIT that can
|
|
1797
|
+
// appear in postfix position (no POST_FOR token exists, unlike
|
|
1798
|
+
// POST_IF / POST_UNLESS). When FOR follows a value-completing token
|
|
1799
|
+
// on the same line (e.g. `addSymbol s for s in xs`), it's a postfix
|
|
1800
|
+
// comprehension that should END the implicit call so the comprehension
|
|
1801
|
+
// wraps the call rather than becoming the call's argument. Detect
|
|
1802
|
+
// postfix FOR by: not at the start of a new line, AND the previous
|
|
1803
|
+
// token is a value-completing token (IDENTIFIER, ), ], literal, etc.).
|
|
1804
|
+
// Prefix FOR (after `:`, `=`, `->`, comma, etc.) keeps the existing
|
|
1805
|
+
// CONTROL behaviour. Falling through lets the IMPLICIT_END handler
|
|
1806
|
+
// below close the implicit call.
|
|
1807
|
+
let isPostfixFor = tag === 'FOR' && !token.newLine && VALUE_END_TAGS.has(prevTag);
|
|
1808
|
+
if ((inImplicitCall() || inImplicitObject()) && CONTROL_IN_IMPLICIT.has(tag) && !isPostfixFor) {
|
|
1787
1809
|
stack.push(['CONTROL', i, {ours: true}]);
|
|
1788
1810
|
return forward(1);
|
|
1789
1811
|
}
|
package/src/parser.js
CHANGED
|
@@ -256,16 +256,16 @@ const parserInstance = {
|
|
|
256
256
|
}
|
|
257
257
|
},
|
|
258
258
|
parse(input) {
|
|
259
|
-
let EOF, TERROR, action, errStr, expected, len, lex, lexer, loc, locs, newState, p, parseTable, preErrorSymbol, r, recovering, rv, sharedState, state, stk, symbol, tokenLen, tokenLine, tokenLoc, tokenText, vals;
|
|
259
|
+
let EOF, TERROR, action, errStr, expected, k, len, lex, lexer, loc, locs, newState, p, parseTable, preErrorSymbol, r, recovering, rv, sharedState, state, stk, symbol, tokenLen, tokenLine, tokenLoc, tokenText, v, vals;
|
|
260
260
|
[stk, vals, locs] = [[0], [null], []];
|
|
261
261
|
[parseTable, tokenText, tokenLine, tokenLen, recovering] = [this.parseTable, "", 0, 0, 0];
|
|
262
262
|
[TERROR, EOF] = [2, 1];
|
|
263
263
|
lexer = Object.create(this.lexer);
|
|
264
264
|
sharedState = { ctx: {} };
|
|
265
|
-
for (
|
|
265
|
+
for (let k in this.ctx) {
|
|
266
266
|
if (!Object.hasOwn(this.ctx, k))
|
|
267
267
|
continue;
|
|
268
|
-
|
|
268
|
+
let v = this.ctx[k];
|
|
269
269
|
sharedState.ctx[k] = v;
|
|
270
270
|
}
|
|
271
271
|
lexer.setInput(input, sharedState.ctx);
|
|
@@ -294,7 +294,7 @@ const parserInstance = {
|
|
|
294
294
|
if (!recovering)
|
|
295
295
|
expected = (() => {
|
|
296
296
|
const result = [];
|
|
297
|
-
for (
|
|
297
|
+
for (let p in parseTable[state]) {
|
|
298
298
|
if (!Object.hasOwn(parseTable[state], p))
|
|
299
299
|
continue;
|
|
300
300
|
if (this.tokenNames[p] && p > TERROR)
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// Schema .d.ts emission — CLI / typecheck only.
|
|
2
|
+
//
|
|
3
|
+
// This module is a CLI/editor-only sidecar that walks parsed schema
|
|
4
|
+
// s-expressions and emits TypeScript declarations for the LSP and
|
|
5
|
+
// `rip check`. The browser bundle must NOT import this module — see
|
|
6
|
+
// scripts/check-bundle-graph.js.
|
|
7
|
+
//
|
|
8
|
+
// SCHEMA_INTRINSIC_DECLS holds the Schema<Out, In> / SchemaIssue /
|
|
9
|
+
// SchemaSafeResult / SchemaQuery / ModelSchema interface declarations
|
|
10
|
+
// that prepend a schema-using compilation. emitSchemaTypes() walks
|
|
11
|
+
// the parsed s-expression, builds per-schema descriptors, and emits
|
|
12
|
+
// `declare const Foo: Schema<...>` / ModelSchema / etc. lines.
|
|
13
|
+
//
|
|
14
|
+
// All the runtime (parsing, validation, ORM, DDL, registry) lives in
|
|
15
|
+
// ./schema.js and the runtime-*.js fragments in this same directory,
|
|
16
|
+
// shared between browser and server. The `runtime-*` files are concatenated
|
|
17
|
+
// at build into runtime.generated.js and execute at runtime; THIS file is
|
|
18
|
+
// orthogonal — it runs at compile time and never reaches runtime.
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Shadow TypeScript — Phase 3.5
|
|
22
|
+
// ============================================================================
|
|
23
|
+
//
|
|
24
|
+
// Emits virtual `.d.ts` / `.ts` declarations for :input, :shape, and :enum
|
|
25
|
+
// schemas so the TS language service can offer autocomplete and catch
|
|
26
|
+
// AST-shape mistakes before Phase 4 layers in :model/ORM/algebra. Written
|
|
27
|
+
// to mirror `emitComponentTypes()` in src/types.js — same prototype:
|
|
28
|
+
// `emitSchemaTypes(sexpr, lines)` returns true when any schema declaration
|
|
29
|
+
// was found (drives preamble injection), mutates `lines` with declarations.
|
|
30
|
+
//
|
|
31
|
+
// Type surface (locked with peer AI):
|
|
32
|
+
//
|
|
33
|
+
// interface Schema<T> {
|
|
34
|
+
// parse(data: unknown): T;
|
|
35
|
+
// safe(data: unknown): SchemaSafeResult<T>;
|
|
36
|
+
// ok(data: unknown): boolean;
|
|
37
|
+
// }
|
|
38
|
+
//
|
|
39
|
+
// `:input` emits declare const Foo: Schema<FooValue>;
|
|
40
|
+
// `:shape` emits declare const Foo: Schema<FooInstance>; where
|
|
41
|
+
// FooInstance = FooData & {methods/readonly getters}.
|
|
42
|
+
// `:enum` emits declare const Role: { parse(...): Role; ok(d): d is Role; ... }
|
|
43
|
+
//
|
|
44
|
+
// Methods are typed `(...args: any[]) => unknown`. Computed are
|
|
45
|
+
// `readonly name: unknown`. Body inference is out of scope for 3.5.
|
|
46
|
+
|
|
47
|
+
export const SCHEMA_INTRINSIC_DECLS = [
|
|
48
|
+
'interface SchemaIssue { field: string; error: string; message: string; }',
|
|
49
|
+
'type SchemaSafeResult<T> = { ok: true; value: T; errors: null } | { ok: false; value: null; errors: SchemaIssue[] };',
|
|
50
|
+
// Base Schema interface. `Out` is the parsed value type; `In` is the
|
|
51
|
+
// data shape (defaults to unknown). Algebra methods are parameterized
|
|
52
|
+
// over `In` so chained operations on a typed :shape or :model derive
|
|
53
|
+
// correctly; when `In` defaults to unknown, `keyof In` is `never` and
|
|
54
|
+
// algebra methods don't autocomplete — which is the right behavior
|
|
55
|
+
// for :input schemas where the input shape isn't statically known.
|
|
56
|
+
'interface Schema<Out, In = unknown> {',
|
|
57
|
+
' parse(data: In): Out;',
|
|
58
|
+
' safe(data: In): SchemaSafeResult<Out>;',
|
|
59
|
+
' ok(data: unknown): boolean;',
|
|
60
|
+
' pick<K extends keyof In>(...keys: K[]): Schema<Pick<In, K>, Pick<In, K>>;',
|
|
61
|
+
' omit<K extends keyof In>(...keys: K[]): Schema<Omit<In, K>, Omit<In, K>>;',
|
|
62
|
+
' partial(): Schema<Partial<In>, Partial<In>>;',
|
|
63
|
+
' required<K extends keyof In>(...keys: K[]): Schema<Omit<In, K> & Required<Pick<In, K>>, Omit<In, K> & Required<Pick<In, K>>>;',
|
|
64
|
+
' extend<U>(other: Schema<U>): Schema<In & U, In & U>;',
|
|
65
|
+
'}',
|
|
66
|
+
// Chainable query builder for :model.
|
|
67
|
+
'interface SchemaQuery<T> {',
|
|
68
|
+
' all(): Promise<T[]>;',
|
|
69
|
+
' first(): Promise<T | null>;',
|
|
70
|
+
' count(): Promise<number>;',
|
|
71
|
+
' limit(n: number): SchemaQuery<T>;',
|
|
72
|
+
' offset(n: number): SchemaQuery<T>;',
|
|
73
|
+
' order(spec: string): SchemaQuery<T>;',
|
|
74
|
+
'}',
|
|
75
|
+
// ModelSchema extends the base schema surface with ORM methods. Algebra
|
|
76
|
+
// over `Data` (not `Instance`) so derived shapes reflect runtime
|
|
77
|
+
// behavior-dropping semantics.
|
|
78
|
+
'interface ModelSchema<Instance, Data = unknown> extends Schema<Instance, Data> {',
|
|
79
|
+
' find(id: unknown): Promise<Instance | null>;',
|
|
80
|
+
' findMany(ids: unknown[]): Promise<Instance[]>;',
|
|
81
|
+
' where(cond: Record<string, unknown> | string, ...params: unknown[]): SchemaQuery<Instance>;',
|
|
82
|
+
' all(limit?: number): Promise<Instance[]>;',
|
|
83
|
+
' first(): Promise<Instance | null>;',
|
|
84
|
+
' count(cond?: Record<string, unknown>): Promise<number>;',
|
|
85
|
+
' create(data: Partial<Data>): Promise<Instance>;',
|
|
86
|
+
' toSQL(options?: { dropFirst?: boolean; header?: string; idStart?: number }): string;',
|
|
87
|
+
'}',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const RIP_TYPE_TO_TS = {
|
|
91
|
+
string: 'string',
|
|
92
|
+
text: 'string',
|
|
93
|
+
email: 'string',
|
|
94
|
+
url: 'string',
|
|
95
|
+
uuid: 'string',
|
|
96
|
+
phone: 'string',
|
|
97
|
+
zip: 'string',
|
|
98
|
+
number: 'number',
|
|
99
|
+
integer: 'number',
|
|
100
|
+
boolean: 'boolean',
|
|
101
|
+
date: 'Date',
|
|
102
|
+
datetime: 'Date',
|
|
103
|
+
json: 'unknown',
|
|
104
|
+
any: 'any',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function mapFieldType(entry) {
|
|
108
|
+
if (entry.typeName === 'literal-union' && entry.literals?.length) {
|
|
109
|
+
return entry.literals.map(l => JSON.stringify(l)).join(' | ');
|
|
110
|
+
}
|
|
111
|
+
let base = RIP_TYPE_TO_TS[entry.typeName] ?? entry.typeName;
|
|
112
|
+
return entry.array ? `${base}[]` : base;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Extract descriptor from a SCHEMA_BODY s-expr node. Grammar reduces
|
|
116
|
+
// `['schema', SCHEMA_BODY_VAL]` where the value is the String wrapper
|
|
117
|
+
// carrying `.descriptor` via the metadata bridge.
|
|
118
|
+
function descriptorFromSchemaNode(schemaNode) {
|
|
119
|
+
if (!Array.isArray(schemaNode)) return null;
|
|
120
|
+
let head = schemaNode[0]?.valueOf?.() ?? schemaNode[0];
|
|
121
|
+
if (head !== 'schema') return null;
|
|
122
|
+
let body = schemaNode[1];
|
|
123
|
+
if (!body || typeof body !== 'object') return null;
|
|
124
|
+
if (body.descriptor) return body.descriptor;
|
|
125
|
+
if (body.data?.descriptor) return body.data.descriptor;
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Walk the parsed s-expression collecting every named schema declaration.
|
|
130
|
+
// Mixins are emitted first so subsequent :shape/:model type aliases can
|
|
131
|
+
// reference them in `& Timestamps`-style intersections. Within a group,
|
|
132
|
+
// source order is preserved. Returns true when at least one schema was
|
|
133
|
+
// found (drives intrinsic preamble injection).
|
|
134
|
+
export function emitSchemaTypes(sexpr, lines) {
|
|
135
|
+
const collected = [];
|
|
136
|
+
collectSchemas(sexpr, collected);
|
|
137
|
+
if (!collected.length) return false;
|
|
138
|
+
|
|
139
|
+
// Set of locally-known schema names (for relation-accessor type
|
|
140
|
+
// resolution — same-file targets get typed, unknown targets degrade).
|
|
141
|
+
const known = new Set(collected.map(c => c.name));
|
|
142
|
+
const byName = new Map(collected.map(c => [c.name, c]));
|
|
143
|
+
|
|
144
|
+
// Mixin types first so type aliases down-file can reference them.
|
|
145
|
+
for (const c of collected) {
|
|
146
|
+
if (c.descriptor.kind === 'mixin') emitOneSchemaType(c, byName, known, lines);
|
|
147
|
+
}
|
|
148
|
+
for (const c of collected) {
|
|
149
|
+
if (c.descriptor.kind !== 'mixin') emitOneSchemaType(c, byName, known, lines);
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function collectSchemas(sexpr, out) {
|
|
155
|
+
if (!Array.isArray(sexpr)) return;
|
|
156
|
+
const head = sexpr[0]?.valueOf?.() ?? sexpr[0];
|
|
157
|
+
let exported = false;
|
|
158
|
+
let assignNode = null;
|
|
159
|
+
if (head === 'export' && Array.isArray(sexpr[1])) {
|
|
160
|
+
const inner = sexpr[1];
|
|
161
|
+
const innerHead = inner[0]?.valueOf?.() ?? inner[0];
|
|
162
|
+
if (innerHead === '=') { exported = true; assignNode = inner; }
|
|
163
|
+
else collectSchemas(sexpr[1], out);
|
|
164
|
+
} else if (head === '=') {
|
|
165
|
+
assignNode = sexpr;
|
|
166
|
+
} else if (head === 'program' || head === 'block') {
|
|
167
|
+
for (let i = 1; i < sexpr.length; i++) {
|
|
168
|
+
if (Array.isArray(sexpr[i])) collectSchemas(sexpr[i], out);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (assignNode && Array.isArray(assignNode[2])) {
|
|
172
|
+
const name = assignNode[1]?.valueOf?.() ?? assignNode[1];
|
|
173
|
+
const descriptor = descriptorFromSchemaNode(assignNode[2]);
|
|
174
|
+
if (typeof name === 'string' && descriptor) {
|
|
175
|
+
out.push({ name, descriptor, exported });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function emitOneSchemaType(collected, byName, known, lines) {
|
|
181
|
+
const { name, descriptor, exported } = collected;
|
|
182
|
+
const exp = exported ? 'export ' : '';
|
|
183
|
+
const decl = exported ? '' : 'declare ';
|
|
184
|
+
|
|
185
|
+
if (descriptor.kind === 'enum') {
|
|
186
|
+
const members = [];
|
|
187
|
+
for (const e of descriptor.entries) {
|
|
188
|
+
if (e.tag !== 'enum-member') continue;
|
|
189
|
+
const v = e.value !== undefined ? e.value : e.name;
|
|
190
|
+
members.push(typeof v === 'string' ? JSON.stringify(v) : String(v));
|
|
191
|
+
}
|
|
192
|
+
const union = members.length ? members.join(' | ') : 'never';
|
|
193
|
+
lines.push(`${exp}type ${name} = ${union};`);
|
|
194
|
+
lines.push(`${exp}${decl}const ${name}: { parse(data: unknown): ${name}; safe(data: unknown): SchemaSafeResult<${name}>; ok(data: unknown): data is ${name}; };`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (descriptor.kind === 'mixin') {
|
|
199
|
+
// :mixin is declaration-time-only; expose it as a field type alias
|
|
200
|
+
// so hosts that `@mixin Foo` can intersect it into their Data type.
|
|
201
|
+
// No value declaration — mixins aren't user-facing runtime values.
|
|
202
|
+
const fieldProps = fieldPropList(descriptor);
|
|
203
|
+
lines.push(`${exp}type ${name} = { ${fieldProps.join('; ')} };`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const fieldProps = fieldPropList(descriptor);
|
|
208
|
+
const mixinRefs = mixinIntersections(descriptor, byName);
|
|
209
|
+
const methods = [];
|
|
210
|
+
const computed = [];
|
|
211
|
+
for (const e of descriptor.entries) {
|
|
212
|
+
if (e.tag === 'method') {
|
|
213
|
+
methods.push(`${e.name}: (...args: any[]) => unknown`);
|
|
214
|
+
} else if (e.tag === 'computed') {
|
|
215
|
+
computed.push(`readonly ${e.name}: unknown`);
|
|
216
|
+
}
|
|
217
|
+
// hooks are intentionally omitted — they fire automatically and
|
|
218
|
+
// shouldn't appear in autocomplete.
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const dataBase = `{ ${fieldProps.join('; ')} }`;
|
|
222
|
+
const dataType = mixinRefs.length ? `${dataBase} & ${mixinRefs.join(' & ')}` : dataBase;
|
|
223
|
+
|
|
224
|
+
if (descriptor.kind === 'model') {
|
|
225
|
+
const dataName = `${name}Data`;
|
|
226
|
+
const instName = `${name}Instance`;
|
|
227
|
+
const relationAccessors = modelRelationAccessors(descriptor, known);
|
|
228
|
+
const instanceExtras = [
|
|
229
|
+
...computed,
|
|
230
|
+
...methods,
|
|
231
|
+
...relationAccessors,
|
|
232
|
+
`save(): Promise<${instName}>`,
|
|
233
|
+
`destroy(): Promise<${instName}>`,
|
|
234
|
+
`ok(): boolean`,
|
|
235
|
+
`errors(): SchemaIssue[]`,
|
|
236
|
+
`toJSON(): ${dataName}`,
|
|
237
|
+
];
|
|
238
|
+
lines.push(`${exp}type ${dataName} = ${dataType};`);
|
|
239
|
+
lines.push(`${exp}type ${instName} = ${dataName} & { ${instanceExtras.join('; ')} };`);
|
|
240
|
+
lines.push(`${exp}${decl}const ${name}: ModelSchema<${instName}, ${dataName}>;`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (descriptor.kind === 'shape') {
|
|
245
|
+
const dataName = `${name}Data`;
|
|
246
|
+
const instName = `${name}Instance`;
|
|
247
|
+
const hasBehavior = methods.length + computed.length > 0;
|
|
248
|
+
lines.push(`${exp}type ${dataName} = ${dataType};`);
|
|
249
|
+
if (hasBehavior) {
|
|
250
|
+
lines.push(`${exp}type ${instName} = ${dataName} & { ${[...computed, ...methods].join('; ')} };`);
|
|
251
|
+
lines.push(`${exp}${decl}const ${name}: Schema<${instName}, ${dataName}>;`);
|
|
252
|
+
} else {
|
|
253
|
+
lines.push(`${exp}${decl}const ${name}: Schema<${dataName}, ${dataName}>;`);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// :input — parse returns the Data shape directly (no behavior).
|
|
259
|
+
const valueName = `${name}Value`;
|
|
260
|
+
lines.push(`${exp}type ${valueName} = ${dataType};`);
|
|
261
|
+
lines.push(`${exp}${decl}const ${name}: Schema<${valueName}, ${valueName}>;`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Return an array of mixin type-reference strings for `& Foo & Bar` joins.
|
|
265
|
+
function mixinIntersections(descriptor, byName) {
|
|
266
|
+
const refs = [];
|
|
267
|
+
for (const e of descriptor.entries) {
|
|
268
|
+
if (e.tag !== 'directive' || e.name !== 'mixin') continue;
|
|
269
|
+
const args = e.args;
|
|
270
|
+
const target = args && args[0] && args[0].target;
|
|
271
|
+
if (!target) continue;
|
|
272
|
+
const known = byName && byName.get(target);
|
|
273
|
+
if (known && known.descriptor.kind === 'mixin') {
|
|
274
|
+
refs.push(target);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return refs;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Emit relation accessor type declarations for :model instances. For
|
|
281
|
+
// targets declared in the same file we emit a typed Promise; for
|
|
282
|
+
// unknown (cross-file) targets we degrade to `Promise<unknown>` rather
|
|
283
|
+
// than emit an unresolved bare name.
|
|
284
|
+
function modelRelationAccessors(descriptor, known) {
|
|
285
|
+
const out = [];
|
|
286
|
+
for (const e of descriptor.entries) {
|
|
287
|
+
if (e.tag !== 'directive') continue;
|
|
288
|
+
const args = e.args;
|
|
289
|
+
if (!args || !args[0]) continue;
|
|
290
|
+
const target = args[0].target;
|
|
291
|
+
if (!target) continue;
|
|
292
|
+
const optional = args[0].optional === true;
|
|
293
|
+
const targetLc = target[0].toLowerCase() + target.slice(1);
|
|
294
|
+
const instName = `${target}Instance`;
|
|
295
|
+
const isKnown = known && known.has(target);
|
|
296
|
+
if (e.name === 'belongs_to') {
|
|
297
|
+
const retT = isKnown ? (optional ? `${instName} | null` : `${instName} | null`) : 'unknown';
|
|
298
|
+
out.push(`${targetLc}(): Promise<${retT}>`);
|
|
299
|
+
} else if (e.name === 'has_one' || e.name === 'one') {
|
|
300
|
+
const retT = isKnown ? `${instName} | null` : 'unknown';
|
|
301
|
+
out.push(`${targetLc}(): Promise<${retT}>`);
|
|
302
|
+
} else if (e.name === 'has_many' || e.name === 'many') {
|
|
303
|
+
const retT = isKnown ? `${instName}[]` : 'unknown[]';
|
|
304
|
+
const pluralLc = __schemaClientPluralize(targetLc);
|
|
305
|
+
out.push(`${pluralLc}(): Promise<${retT}>`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Minimal pluralizer for accessor names. Keep in sync with the runtime
|
|
312
|
+
// __schemaPluralize rules (same surface for declaration parity).
|
|
313
|
+
function __schemaClientPluralize(w) {
|
|
314
|
+
const lw = w.toLowerCase();
|
|
315
|
+
if (/[^aeiouy]y$/i.test(w)) return w.slice(0, -1) + 'ies';
|
|
316
|
+
if (/(s|x|z|ch|sh)$/i.test(w)) return w + 'es';
|
|
317
|
+
return w + 's';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function fieldPropList(descriptor) {
|
|
321
|
+
const props = [];
|
|
322
|
+
for (const e of descriptor.entries) {
|
|
323
|
+
if (e.tag !== 'field') continue;
|
|
324
|
+
const required = e.modifiers.includes('!');
|
|
325
|
+
const mark = required ? '' : '?';
|
|
326
|
+
props.push(`${e.name}${mark}: ${mapFieldType(e)}`);
|
|
327
|
+
}
|
|
328
|
+
return props;
|
|
329
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Schema runtime loader — browser variant.
|
|
2
|
+
//
|
|
3
|
+
// Why this file exists: this is the IMPORT BOUNDARY for the browser
|
|
4
|
+
// bundle. By only importing validate + browser-stubs fragments here,
|
|
5
|
+
// Bun's tree-shaker can omit db-naming / orm / ddl from the bundle.
|
|
6
|
+
// If the mode-switch lived in src/schema.js (reachable from every
|
|
7
|
+
// entry), the bundler couldn't statically prove the unused fragments
|
|
8
|
+
// were unreachable and would keep them. The loader split is the lever
|
|
9
|
+
// that makes the bundle savings real.
|
|
10
|
+
//
|
|
11
|
+
// See loader-server.js for the corresponding server / CLI variant
|
|
12
|
+
// that imports all five fragments.
|
|
13
|
+
//
|
|
14
|
+
// Side-effect import. Adds a runtime provider that supports validate
|
|
15
|
+
// and browser modes only. Eagerly installs the browser runtime on
|
|
16
|
+
// globalThis at module load.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
SCHEMA_RUNTIME_WRAPPER_HEAD,
|
|
20
|
+
SCHEMA_RUNTIME_WRAPPER_TAIL,
|
|
21
|
+
SCHEMA_VALIDATE_RUNTIME,
|
|
22
|
+
SCHEMA_BROWSER_STUBS_RUNTIME,
|
|
23
|
+
} from './runtime.generated.js';
|
|
24
|
+
import { setSchemaRuntimeProvider } from './schema.js';
|
|
25
|
+
|
|
26
|
+
function provider({ mode = 'browser' } = {}) {
|
|
27
|
+
let body;
|
|
28
|
+
switch (mode) {
|
|
29
|
+
case 'validate':
|
|
30
|
+
body = SCHEMA_VALIDATE_RUNTIME;
|
|
31
|
+
break;
|
|
32
|
+
case 'browser':
|
|
33
|
+
body = SCHEMA_VALIDATE_RUNTIME + '\n' + SCHEMA_BROWSER_STUBS_RUNTIME;
|
|
34
|
+
break;
|
|
35
|
+
case 'server':
|
|
36
|
+
case 'migration':
|
|
37
|
+
throw new Error(
|
|
38
|
+
"schema runtime mode '" + mode + "' is not available in the browser. " +
|
|
39
|
+
"ORM and DDL features require side-effect-importing loader-server.js."
|
|
40
|
+
);
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`unknown schema runtime mode: ${mode}`);
|
|
43
|
+
}
|
|
44
|
+
return (SCHEMA_RUNTIME_WRAPPER_HEAD + body + SCHEMA_RUNTIME_WRAPPER_TAIL).trimStart();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setSchemaRuntimeProvider(provider);
|
|
48
|
+
|
|
49
|
+
// Eagerly install browser runtime so user code compiled with
|
|
50
|
+
// skipRuntimes: true (the typical case in browser bundles) finds
|
|
51
|
+
// {__schema, SchemaError} on globalThis.
|
|
52
|
+
export const SCHEMA_RUNTIME = provider({ mode: 'browser' });
|
|
53
|
+
if (typeof globalThis !== 'undefined' && !globalThis.__ripSchema) {
|
|
54
|
+
try { (0, eval)(SCHEMA_RUNTIME); } catch {}
|
|
55
|
+
}
|