rip-lang 3.15.0 → 3.15.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 -2
- package/bin/rip +1 -1
- package/docs/RIP-SCHEMA.md +4 -4
- package/docs/dist/rip.js +6 -6
- package/docs/dist/rip.min.js +83 -83
- package/docs/dist/rip.min.js.br +0 -0
- 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 +6 -8
- package/scripts/postinstall.js +27 -0
- package/src/AGENTS.md +27 -25
- package/src/browser.js +1 -1
- package/src/compiler.js +1 -1
- package/src/grammar/grammar.rip +1 -1
- package/src/lexer.js +1 -1
- 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/schema.js +1805 -0
- package/src/typecheck.js +2 -2
- package/src/types-emit.js +1 -1
package/docs/dist/rip.min.js.br
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rip-lang",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.2",
|
|
4
4
|
"description": "A modern language that compiles to JavaScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/compiler.js",
|
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
},
|
|
18
18
|
"./loader": "./rip-loader.js"
|
|
19
19
|
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@rip-lang/schema": "0.1.0"
|
|
22
|
-
},
|
|
23
20
|
"bin": {
|
|
24
21
|
"rip": "bin/rip"
|
|
25
22
|
},
|
|
@@ -27,6 +24,7 @@
|
|
|
27
24
|
"bin/",
|
|
28
25
|
"src/",
|
|
29
26
|
"docs/",
|
|
27
|
+
"scripts/postinstall.js",
|
|
30
28
|
"scripts/serve.js",
|
|
31
29
|
"rip-loader.js",
|
|
32
30
|
"README.md",
|
|
@@ -39,16 +37,16 @@
|
|
|
39
37
|
"gallery": "bun scripts/gallery.js",
|
|
40
38
|
"bundle:demo": "bun scripts/bundle-app.js docs/demo -o docs/example/index.json -t 'Rip App Demo'",
|
|
41
39
|
"parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
|
|
42
|
-
"postinstall": "
|
|
40
|
+
"postinstall": "node scripts/postinstall.js --quiet",
|
|
43
41
|
"link-local": "bun scripts/link-local.js",
|
|
44
42
|
"link-global": "bun scripts/link-global.js",
|
|
45
43
|
"link-check": "bun scripts/link-check.js",
|
|
46
44
|
"serve": "bun scripts/serve.js",
|
|
47
45
|
"test": "bun test/runner.js",
|
|
48
46
|
"test:types": "bun test/types/runner.js",
|
|
49
|
-
"test:schema": "bun
|
|
50
|
-
"test:schema-fresh": "bun
|
|
51
|
-
"build:schema-runtime": "bun
|
|
47
|
+
"test:schema": "bun test/schema/singleton.test.js && bun test/schema/errors.test.js && bun test/schema/modes.test.js && bun test/schema/build.test.js",
|
|
48
|
+
"test:schema-fresh": "bun scripts/build-schema-runtime.js --check",
|
|
49
|
+
"build:schema-runtime": "bun scripts/build-schema-runtime.js",
|
|
52
50
|
"test:bundle": "bun test/bundle.test.js",
|
|
53
51
|
"test:graph": "bun scripts/check-bundle-graph.js",
|
|
54
52
|
"test:server": "./bin/rip packages/server/tests/runner.rip",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/postinstall.js — workspace-only postinstall hook.
|
|
3
|
+
//
|
|
4
|
+
// `link-local.js` and `link-check.js` rewrite `node_modules/.bun/` and
|
|
5
|
+
// `node_modules/rip-lang/` to point at the workspace's source tree.
|
|
6
|
+
// That's exactly what we want during dev, but it's meaningless — and
|
|
7
|
+
// would error — for consumers running `npm install rip-lang` from
|
|
8
|
+
// outside this repo (no `packages/` sibling, possibly no `bun` binary).
|
|
9
|
+
//
|
|
10
|
+
// This dispatcher checks for the workspace markers first and exits 0
|
|
11
|
+
// otherwise. Uses Node only (every npm install has Node; not every
|
|
12
|
+
// consumer has Bun). Tolerates missing files / non-zero subprocess
|
|
13
|
+
// exits — postinstall is best-effort, never fatal for consumers.
|
|
14
|
+
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
if (!existsSync('packages') || !existsSync('scripts/link-local.js')) process.exit(0);
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const run = (script) => {
|
|
22
|
+
const r = spawnSync('bun', ['scripts/' + script, ...args], { stdio: 'inherit' });
|
|
23
|
+
return r.status ?? 0;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
run('link-local.js');
|
|
27
|
+
run('link-check.js');
|
package/src/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Compiler Subsystem — Agent Guide
|
|
2
2
|
|
|
3
|
-
This covers `compiler.js`, `lexer.js`, `components.js`, `browser.js`, `types.js`, `types-emit.js`, `app.rip`, `typecheck.js`, and the `grammar/` directory. The schema feature lives in `
|
|
3
|
+
This covers `compiler.js`, `lexer.js`, `components.js`, `browser.js`, `types.js`, `types-emit.js`, `app.rip`, `typecheck.js`, the `schema/` subdirectory, and the `grammar/` directory. The schema feature lives in `src/schema/` (entry `src/schema/schema.js`, imported via relative paths like `./schema/schema.js` from sibling modules).
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -16,22 +16,22 @@ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus
|
|
|
16
16
|
| `src/lexer.js` | yes | tokenizer + rewriter pipeline |
|
|
17
17
|
| `src/compiler.js` | yes | codegen + reactive runtime + component runtime + `compileToJS` + `setTypesEmitter` hook + `emitEnum` |
|
|
18
18
|
| `src/components.js` | yes | render rewriter + component runtime |
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
19
|
+
| `src/schema/schema.js` | yes (via `./schema/schema.js`) | lexer rewrite + body parser + emitSchema codegen + `setSchemaRuntimeProvider` hook (no fragment imports) |
|
|
20
|
+
| `src/schema/loader-browser.js` | yes (browser only, via `./schema/loader-browser.js`) | imports validate + browser-stubs fragments; eager-installs browser runtime; registers provider |
|
|
21
|
+
| `src/schema/loader-server.js` | **no** (CLI / server / tests, via `./schema/loader-server.js`) | imports all five fragments; eager-installs migration runtime; registers provider |
|
|
22
|
+
| `src/schema/runtime.generated.js` | yes (browser uses 2 of 5 exports) | autogen from `runtime-*.js` fragments; CI staleness check via `bun run test:schema-fresh` |
|
|
23
|
+
| `src/schema/runtime-validate.js` | source for the `validate` fragment (universal) |
|
|
24
|
+
| `src/schema/runtime-db-naming.js` | source for `db-naming` fragment (server + migration) |
|
|
25
|
+
| `src/schema/runtime-orm.js` | source for `orm` fragment (server + migration) |
|
|
26
|
+
| `src/schema/runtime-ddl.js` | source for `ddl` fragment (migration only) |
|
|
27
|
+
| `src/schema/runtime-browser-stubs.js` | source for `browser-stubs` fragment (browser only) |
|
|
28
28
|
| `src/types.js` | yes | only `installTypeSupport(Lexer)` — token-stream type stripper |
|
|
29
29
|
| `src/error.js` | yes | runtime error formatting |
|
|
30
30
|
| `src/sourcemaps.js` | yes | inline source-map generation |
|
|
31
31
|
| `src/generated/dom-tags.js` | yes | HTML/SVG tag set for render-block tag detection |
|
|
32
32
|
| `src/generated/dom-events.js` | yes | event-name set for `onClick`/`onKeydown` auto-wire |
|
|
33
33
|
| `src/types-emit.js` | **no** | `.d.ts` emitter + intrinsic decl tables — CLI / typecheck only |
|
|
34
|
-
| `
|
|
34
|
+
| `src/schema/dts-emit.js` | **no** | schema `.d.ts` emitter — CLI / typecheck only |
|
|
35
35
|
| `src/typecheck.js` | **no** | TypeScript LSP integration — CLI only |
|
|
36
36
|
| `src/repl.js` | **no** | interactive CLI REPL |
|
|
37
37
|
|
|
@@ -60,7 +60,7 @@ The browser bundle never imports `types-emit.js`, so the emitter stays null and
|
|
|
60
60
|
|
|
61
61
|
**Failure mode to remember:** If you write code that calls `compile(source, { types: 'emit' })` and inspects `result.dts`, you **must** import `src/types-emit.js` (directly or indirectly) somewhere in that code path. Without it, `result.dts` is `null` regardless of source content. Symptom: types emission "silently does nothing" — no error, no warning, just empty output. The fix is one line: `import '../src/types-emit.js';`.
|
|
62
62
|
|
|
63
|
-
The schema runtime uses an analogous hook: `
|
|
63
|
+
The schema runtime uses an analogous hook: `src/schema/schema.js` exports `setSchemaRuntimeProvider(fn)`, default null. `src/schema/loader-server.js` and `src/schema/loader-browser.js` are the two providers. CLI / tests / server side-effect-import `./schema/loader-server.js` (full migration runtime, all four modes). The browser bundle (`src/browser.js`) side-effect-imports `./schema/loader-browser.js` (validate + browser-stubs only). Same failure mode applies — call `getSchemaRuntime()` without registering a provider and you get a clear error pointing at which loader to import.
|
|
64
64
|
|
|
65
65
|
The mode matrix exposed by `getSchemaRuntime({ mode })`:
|
|
66
66
|
|
|
@@ -71,7 +71,7 @@ The mode matrix exposed by `getSchemaRuntime({ mode })`:
|
|
|
71
71
|
| `server` | VALIDATE + DB_NAMING + ORM | `@rip-lang/server` and friends |
|
|
72
72
|
| `migration` | VALIDATE + DB_NAMING + ORM + DDL | CLI / migration tool / tests (default) |
|
|
73
73
|
|
|
74
|
-
Edits to `
|
|
74
|
+
Edits to `src/schema/runtime-*.js` require running `bun run build:schema-runtime` to regenerate `runtime.generated.js`. CI fails (`bun run test:schema-fresh`) if the generated file is stale.
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
@@ -643,29 +643,31 @@ Types are processed at the token layer before parsing.
|
|
|
643
643
|
## Schema System
|
|
644
644
|
|
|
645
645
|
Inline schemas are a compiler sidecar that parallels `types.js` and
|
|
646
|
-
`components.js
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
646
|
+
`components.js` — a feature of the rip-lang compiler, just organized
|
|
647
|
+
into its own subdirectory `src/schema/` because of its size. Schema
|
|
648
|
+
mutates the host parser's lexer to re-parse `@ensure` predicate
|
|
649
|
+
bodies, so it isn't a clean separable package; the subdirectory is
|
|
650
|
+
purely for source organization. The implementation is split across
|
|
651
|
+
several files by execution context:
|
|
652
|
+
|
|
653
|
+
- `src/schema/schema.js` (browser + server) — lexer rewrite,
|
|
651
654
|
body parsers, `emitSchema` codegen, and `setSchemaRuntimeProvider`
|
|
652
655
|
hook. The runtime body itself is **not** here; this file imports zero
|
|
653
656
|
fragments so the bundler can decide per-entry which fragments to include.
|
|
654
|
-
- `
|
|
655
|
-
(sources) and `
|
|
657
|
+
- `src/schema/runtime-{validate,db-naming,orm,ddl,browser-stubs}.js`
|
|
658
|
+
(sources) and `src/schema/runtime.generated.js` (autogen) —
|
|
656
659
|
five runtime fragments composed at call time by `getSchemaRuntime({ mode })`.
|
|
657
660
|
Edit a source fragment, run `bun run build:schema-runtime` to refresh
|
|
658
661
|
the generated file, commit. CI's `test:schema-fresh` fails on staleness.
|
|
659
|
-
-
|
|
662
|
+
- `src/schema/loader-server.js` and `src/schema/loader-browser.js`
|
|
660
663
|
— the import boundary that decides which fragments end up in which
|
|
661
664
|
bundle. Server loader pulls all five; browser loader pulls only
|
|
662
665
|
validate + browser-stubs. Bun's tree-shaker uses these import sets to
|
|
663
666
|
omit server-only fragments from `docs/dist/rip.min.js`.
|
|
664
|
-
-
|
|
667
|
+
- `src/schema/dts-emit.js` (CLI/LSP only) — `emitSchemaTypes` walks
|
|
665
668
|
parsed schema s-expressions and emits `declare const Foo: Schema<...>`
|
|
666
669
|
lines for the TypeScript language service. Imported only by
|
|
667
|
-
`types-emit.js` and `typecheck.js`.
|
|
668
|
-
so all schema-related code is colocated; the `dts-emit` name signals
|
|
670
|
+
`types-emit.js` and `typecheck.js`. The `dts-emit` name signals
|
|
669
671
|
that this is a compile-time `.d.ts` emitter, not a `runtime-*` fragment.
|
|
670
672
|
|
|
671
673
|
### Lexer path
|
|
@@ -765,7 +767,7 @@ signatures both enforce this.
|
|
|
765
767
|
`@ensure` entirely (trusted data). Runtime method name
|
|
766
768
|
`_applyEnsures` mirrors the directive (parallel to
|
|
767
769
|
`_applyTransforms` for `-> transform` and `_applyEagerDerived` for
|
|
768
|
-
`!> derived`). See `
|
|
770
|
+
`!> derived`). See `src/schema/schema.js`.
|
|
769
771
|
|
|
770
772
|
### Shadow TS
|
|
771
773
|
|
package/src/browser.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Pulls in only the validate + browser-stubs fragments; tree-shakes
|
|
6
6
|
// db-naming, orm, and ddl fragments out of the bundle. Must be the
|
|
7
7
|
// first import so any later module-load eager-installs see it.
|
|
8
|
-
import '
|
|
8
|
+
import './schema/loader-browser.js';
|
|
9
9
|
|
|
10
10
|
export { Lexer } from './lexer.js';
|
|
11
11
|
export { parser } from './parser.js';
|
package/src/compiler.js
CHANGED
|
@@ -16,7 +16,7 @@ import { installComponentSupport } from './components.js';
|
|
|
16
16
|
// so _typesEmitter stays null and .d.ts output is silently skipped.
|
|
17
17
|
let _typesEmitter = null;
|
|
18
18
|
export function setTypesEmitter(fn) { _typesEmitter = fn; }
|
|
19
|
-
import { installSchemaSupport } from '
|
|
19
|
+
import { installSchemaSupport } from './schema/schema.js';
|
|
20
20
|
import { SourceMapGenerator } from './sourcemaps.js';
|
|
21
21
|
import { RipError, toRipError } from './error.js';
|
|
22
22
|
|
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 (
|
|
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/lexer.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// ==========================================================================
|
|
41
41
|
|
|
42
42
|
import { installTypeSupport } from './types.js';
|
|
43
|
-
import { installSchemaSupport } from '
|
|
43
|
+
import { installSchemaSupport } from './schema/schema.js';
|
|
44
44
|
|
|
45
45
|
// ==========================================================================
|
|
46
46
|
// Token Category Sets
|
|
@@ -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
|
+
}
|