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/docs/dist/rip.min.js.br
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -32,7 +32,8 @@ cursor --install-extension ./rip-latest.vsix
|
|
|
32
32
|
|
|
33
33
|
<h2>Versions</h2>
|
|
34
34
|
<ul>
|
|
35
|
-
<li><a href="./rip-0.
|
|
35
|
+
<li><a href="./vscode-rip-0.6.0.vsix"><code>vscode-rip-0.6.0.vsix</code></a></li>
|
|
36
|
+
<li><a href="./rip-0.5.15.vsix"><code>rip-0.5.15.vsix</code></a></li>
|
|
36
37
|
</ul>
|
|
37
38
|
|
|
38
39
|
<footer>← <a href="../../">all rip-lang extensions</a></footer>
|
|
Binary file
|
|
Binary file
|
package/docs/index.html
CHANGED
|
@@ -634,7 +634,7 @@
|
|
|
634
634
|
typeKeywords: ['number', 'string', 'boolean', 'void', 'any', 'never', 'unknown', 'object', 'symbol', 'bigint'],
|
|
635
635
|
|
|
636
636
|
operators: [
|
|
637
|
-
'
|
|
637
|
+
'::', ':=', '~=', '~>', '<=>', '=!', '!?', '=~', '??', '?.', '...',
|
|
638
638
|
'..', '**', '//', '%%', '++', '--', '&&', '||', '===', '!==', '==', '!=',
|
|
639
639
|
'<=', '>=', '=>', '->', '+=', '-=', '*=', '/=', '%=', '**=', '&&=', '||=',
|
|
640
640
|
'??=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', '>>>', '>>', '<<',
|
package/docs/ui/hljs-rip.js
CHANGED
|
@@ -165,7 +165,7 @@ export default function(hljs) {
|
|
|
165
165
|
|
|
166
166
|
const OPERATORS = {
|
|
167
167
|
className: 'operator',
|
|
168
|
-
begin:
|
|
168
|
+
begin: /::|:=|~=|~>|<=>|\.=|=!|!\?|\?!|=~|\?\?=|\?\?|\?\.|\.\.\.|\.\.|=>|->|\*\*|\/\/|%%|===|!==|==|!=|<=|>=|&&|\|\||[+\-*\/%&|^~<>=!?]/,
|
|
169
169
|
relevance: 0,
|
|
170
170
|
};
|
|
171
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rip-lang",
|
|
3
|
-
"version": "3.16.
|
|
3
|
+
"version": "3.16.2",
|
|
4
4
|
"description": "A modern language that compiles to JavaScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/compiler.js",
|
|
@@ -49,14 +49,17 @@
|
|
|
49
49
|
"serve": "bun scripts/serve.js",
|
|
50
50
|
"test": "bun test/runner.js",
|
|
51
51
|
"test:types": "bun test/types/runner.js",
|
|
52
|
-
"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",
|
|
52
|
+
"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 && bun test/schema/extract.test.js && bun test/schema/infer.test.js",
|
|
53
53
|
"test:schema-fresh": "bun scripts/build-schema-runtime.js --check",
|
|
54
|
+
"test:docs": "bun scripts/doctest.js",
|
|
54
55
|
"build:schema-runtime": "bun scripts/build-schema-runtime.js",
|
|
55
56
|
"test:bundle": "bun test/bundle.test.js",
|
|
57
|
+
"test:loader": "bun test/loader-reexport.test.js",
|
|
56
58
|
"test:graph": "bun scripts/check-bundle-graph.js",
|
|
57
59
|
"test:server": "./bin/rip packages/server/tests/runner.rip",
|
|
58
60
|
"test:time": "bun run --cwd packages/time test",
|
|
59
|
-
"test:
|
|
61
|
+
"test:validate": "./bin/rip packages/validate/test.rip",
|
|
62
|
+
"test:all": "bun run test && bun run test:types && bun run test:schema && bun run test:schema-fresh && bun run test:docs && bun run test:loader && bun run test:graph && bun run test:bundle && bun run test:server && bun run test:time && bun run test:validate",
|
|
60
63
|
"test:ui": "bun run --cwd packages/ui test:e2e",
|
|
61
64
|
"test:ui:chromium": "bun run --cwd packages/ui test:e2e:chromium",
|
|
62
65
|
"test:ui:axe": "bun run --cwd packages/ui test:e2e:axe",
|
|
@@ -90,7 +93,7 @@
|
|
|
90
93
|
"homepage": "https://github.com/shreeve/rip-lang#readme",
|
|
91
94
|
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
92
95
|
"license": "MIT",
|
|
93
|
-
"
|
|
96
|
+
"dependencies": {
|
|
94
97
|
"@types/bun": "1.3.14",
|
|
95
98
|
"typescript": "catalog:"
|
|
96
99
|
}
|
package/src/AGENTS.md
CHANGED
|
@@ -27,6 +27,7 @@ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus
|
|
|
27
27
|
| `src/schema/runtime-db-naming.js` | source for `db-naming` fragment (server + migration) |
|
|
28
28
|
| `src/schema/runtime-orm.js` | source for `orm` fragment (server + migration) |
|
|
29
29
|
| `src/schema/runtime-ddl.js` | source for `ddl` fragment (migration only) |
|
|
30
|
+
| `src/schema/runtime-migrate.js` | source for `migrate` fragment (migration only) — introspection, differ, status/make/migrate |
|
|
30
31
|
| `src/schema/runtime-browser-stubs.js` | source for `browser-stubs` fragment (browser only) |
|
|
31
32
|
| `src/types.js` | yes | only `installTypeSupport(Lexer)` — token-stream type stripper |
|
|
32
33
|
| `src/error.js` | yes | runtime error formatting |
|
|
@@ -40,6 +41,8 @@ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus
|
|
|
40
41
|
|
|
41
42
|
The forbidden list in `scripts/check-bundle-graph.js` enforces this. If a code change would put a forbidden module on the browser graph, `bun run build` aborts before the bundler runs.
|
|
42
43
|
|
|
44
|
+
**Decision record — "core Rip" vs "type-enabled Rip" (June 2026):** this module graph *is* the split. Tier 0 is the erasure core (everything reachable from `src/browser.js` — types stripped, never checked); tier 1 adds `.d.ts` emission (`dts.js`, registered via `setTypesEmitter`); tier 2 adds type checking (`typecheck.js` + the TypeScript service, loaded only by `rip check`). A package-level split (`@rip-lang/core` + a typed variant) was considered and rejected: it would add publishing and versioning friction to express a boundary the import graph and the bundle-graph guard already enforce mechanically. Don't re-propose the package split; if the boundary needs strengthening, extend the forbidden list or route new type-aware behavior through a registration hook instead.
|
|
45
|
+
|
|
43
46
|
### Registration-hook pattern (`setTypesEmitter`, `setSchemaRuntimeProvider`)
|
|
44
47
|
|
|
45
48
|
The same pattern is used twice — once for `.d.ts` emission, once for the schema runtime body. Both make the bundler's tree-shaker keep CLI/server-only code out of the browser bundle.
|
|
@@ -72,7 +75,7 @@ The mode matrix exposed by `getSchemaRuntime({ mode })`:
|
|
|
72
75
|
| `validate` | VALIDATE | isomorphic validate-only contexts |
|
|
73
76
|
| `browser` | VALIDATE + BROWSER_STUBS | the `<script type="text/rip">` runtime |
|
|
74
77
|
| `server` | VALIDATE + DB_NAMING + ORM | `@rip-lang/server` and friends |
|
|
75
|
-
| `migration` | VALIDATE + DB_NAMING + ORM + DDL | CLI / migration tool / tests (default) |
|
|
78
|
+
| `migration` | VALIDATE + DB_NAMING + ORM + DDL + MIGRATE | CLI / migration tool / tests (default) |
|
|
76
79
|
|
|
77
80
|
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.
|
|
78
81
|
|
|
@@ -236,6 +239,30 @@ Complete node reference:
|
|
|
236
239
|
['render', body]
|
|
237
240
|
```
|
|
238
241
|
|
|
242
|
+
## Rewriter Pipeline (canonical order)
|
|
243
|
+
|
|
244
|
+
The single source of truth is `rewrite()` in `src/lexer.js`. Current order, with
|
|
245
|
+
the passes contributed by sidecar files marked:
|
|
246
|
+
|
|
247
|
+
1. `removeLeadingNewlines`
|
|
248
|
+
2. `rewriteDottedPicks` — must run before `rewriteMapLiterals` so pick bodies see raw `{` `}` for depth counting
|
|
249
|
+
3. `rewriteMapLiterals`
|
|
250
|
+
4. `closeMergeAssignments`
|
|
251
|
+
5. `closeOpenCalls`
|
|
252
|
+
6. `closeOpenIndexes`
|
|
253
|
+
7. `rewriteTypes` — installed by `types.js`; must run before `normalizeLines`, otherwise a type-arrow `=>` inside `(...) => T` is treated as a single-liner function and wrapped in spurious INDENT/OUTDENT, derailing the type collector
|
|
254
|
+
8. `normalizeLines`
|
|
255
|
+
9. `rewriteRender?.()` — installed by `components.js` (optional sidecar)
|
|
256
|
+
10. `rewriteSchema?.()` — installed by `schema/schema.js` (optional sidecar)
|
|
257
|
+
11. `tagPostfixConditionals`
|
|
258
|
+
12. `rewriteTaggedTemplates`
|
|
259
|
+
13. `addImplicitBracesAndParens`
|
|
260
|
+
14. `addImplicitCallCommas`
|
|
261
|
+
|
|
262
|
+
When pass order changes in `rewrite()`, update this list — other sections of
|
|
263
|
+
this file describe pass positions relative to their neighbors and assume this
|
|
264
|
+
listing is accurate.
|
|
265
|
+
|
|
239
266
|
## Lexer Token Format
|
|
240
267
|
|
|
241
268
|
Tokens are `[tag, val]` arrays with extra properties:
|
|
@@ -356,7 +383,7 @@ The component system is a compiler sidecar. `installComponentSupport(CodeEmitter
|
|
|
356
383
|
|
|
357
384
|
### Render Rewriter
|
|
358
385
|
|
|
359
|
-
`rewriteRender()` runs after `normalizeLines` and before `
|
|
386
|
+
`rewriteRender()` runs after `normalizeLines` and before `rewriteSchema()` (see "Rewriter Pipeline" above for the full order).
|
|
360
387
|
|
|
361
388
|
Inside `render` blocks it rewrites template syntax into function-call syntax:
|
|
362
389
|
|
|
@@ -653,7 +680,7 @@ enum Status
|
|
|
653
680
|
Type emission is split across two files by execution context:
|
|
654
681
|
|
|
655
682
|
- `types.js` (browser-side, ~21 KB) — `installTypeSupport(Lexer)` adds `rewriteTypes()` to strip type annotations from the token stream so user-typed Rip parses. This is the only thing the browser needs from type machinery.
|
|
656
|
-
- `dts.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source)` generates `.d.ts`, plus `tsType`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`.
|
|
683
|
+
- `dts.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source, schemaBehavior)` generates `.d.ts`, plus `tsType`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`. `schemaBehavior` (the generator's per-compile buffer of compiled `~>`/`!>` bodies, populated only in `inlineTypes`/shadow mode) drives computed/derived return-type inference.
|
|
657
684
|
|
|
658
685
|
`emitEnum` (runtime JS for `enum` blocks) lives in `compiler.js` next to the rest of the codegen dispatch — it's not type machinery, it's real runtime emission.
|
|
659
686
|
|
|
@@ -700,8 +727,10 @@ several files by execution context:
|
|
|
700
727
|
### Lexer path
|
|
701
728
|
|
|
702
729
|
- `installSchemaSupport(Lexer, CodeEmitter)` adds `rewriteSchema()` to the
|
|
703
|
-
Lexer prototype. It runs
|
|
704
|
-
the rewriter pipeline
|
|
730
|
+
Lexer prototype. It runs immediately after `rewriteRender()` and before
|
|
731
|
+
`tagPostfixConditionals()` in the rewriter pipeline (see "Rewriter
|
|
732
|
+
Pipeline" earlier in this file for the full order; note `rewriteTypes()`
|
|
733
|
+
is not adjacent — it runs much earlier, before `normalizeLines()`).
|
|
705
734
|
- `rewriteSchema()` detects a contextual `schema` identifier at expression-
|
|
706
735
|
start positions followed by either a `:kind` SYMBOL or a direct INDENT.
|
|
707
736
|
The matching INDENT...OUTDENT range is parsed by a schema-specific
|
|
@@ -798,9 +827,11 @@ signatures both enforce this.
|
|
|
798
827
|
|
|
799
828
|
### Shadow TS
|
|
800
829
|
|
|
801
|
-
`emitSchemaTypes(sexpr, lines)` walks the parsed s-expression
|
|
802
|
-
schema declarations, emits mixins first so intersections resolve, then
|
|
803
|
-
emits type aliases and `declare const` per kind
|
|
830
|
+
`emitSchemaTypes(sexpr, lines, schemaBehavior)` walks the parsed s-expression
|
|
831
|
+
for named schema declarations, emits mixins first so intersections resolve, then
|
|
832
|
+
emits type aliases and `declare const` per kind (`schemaBehavior`, when set in
|
|
833
|
+
shadow mode, anchors computed/derived members to
|
|
834
|
+
`ReturnType<typeof __<Name>__behavior.field>`):
|
|
804
835
|
|
|
805
836
|
- `:input` → `Schema<ValueType, ValueType>`
|
|
806
837
|
- `:shape` → `Schema<ShapeInstance, ShapeData>` (or `Schema<Data, Data>` when
|
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 './schema/schema.js';
|
|
19
|
+
import { installSchemaSupport, foldDerivedSchemas } from './schema/schema.js';
|
|
20
20
|
import { SourceMapGenerator } from './sourcemaps.js';
|
|
21
21
|
import { stringify, getStdlibCode } from './stdlib.js';
|
|
22
22
|
import { RipError, toRipError } from './error.js';
|
|
@@ -220,6 +220,7 @@ export class CodeEmitter {
|
|
|
220
220
|
'computed': 'emitComputed',
|
|
221
221
|
'readonly': 'emitReadonly',
|
|
222
222
|
'effect': 'emitEffect',
|
|
223
|
+
'gate': 'emitGate',
|
|
223
224
|
|
|
224
225
|
// Control flow — simple
|
|
225
226
|
'break': 'emitBreak',
|
|
@@ -227,7 +228,6 @@ export class CodeEmitter {
|
|
|
227
228
|
'?': 'emitExistential',
|
|
228
229
|
'presence': 'emitPresence',
|
|
229
230
|
'?:': 'emitTernary',
|
|
230
|
-
'|>': 'emitPipe',
|
|
231
231
|
'loop': 'emitLoop',
|
|
232
232
|
'loop-n': 'emitLoopN',
|
|
233
233
|
'await': 'emitAwait',
|
|
@@ -309,6 +309,11 @@ export class CodeEmitter {
|
|
|
309
309
|
this.functionVars = new Map();
|
|
310
310
|
this.helpers = new Set();
|
|
311
311
|
this.scopeStack = []; // Track enclosing function scopes for proper variable hoisting
|
|
312
|
+
// Opt-in projection folding: rewrite foldable `X = Base.pick(...)` derived
|
|
313
|
+
// schemas into self-contained schema literals before emit (and before DTS,
|
|
314
|
+
// which reads the same s-expr). Off by default — the browser-bundle
|
|
315
|
+
// extractor enables it so projections can cross the client boundary.
|
|
316
|
+
if (this.options.foldProjections) foldDerivedSchemas(sexpr);
|
|
312
317
|
this.collectProgramVariables(sexpr);
|
|
313
318
|
let code = this.emit(sexpr);
|
|
314
319
|
|
|
@@ -1075,9 +1080,9 @@ export class CodeEmitter {
|
|
|
1075
1080
|
|
|
1076
1081
|
if (this.usesTemplates && !skip) {
|
|
1077
1082
|
if (skipRT) {
|
|
1078
|
-
code += 'var { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __Component } = globalThis.__ripComponent;\n';
|
|
1083
|
+
code += 'var { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __gateBind, __Component } = globalThis.__ripComponent;\n';
|
|
1079
1084
|
} else if (typeof globalThis !== 'undefined' && globalThis.__ripComponent) {
|
|
1080
|
-
code += 'const { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __Component } = globalThis.__ripComponent;\n';
|
|
1085
|
+
code += 'const { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __gateBind, __Component } = globalThis.__ripComponent;\n';
|
|
1081
1086
|
} else {
|
|
1082
1087
|
code += this.getComponentRuntime();
|
|
1083
1088
|
}
|
|
@@ -1128,19 +1133,35 @@ export class CodeEmitter {
|
|
|
1128
1133
|
return `${this.emit(left, 'value')}.repeat(${this.emit(right, 'value')})`;
|
|
1129
1134
|
}
|
|
1130
1135
|
}
|
|
1131
|
-
// Chained comparisons: (< (< a b) c) → ((a < b) && (b < c))
|
|
1132
|
-
|
|
1133
|
-
|
|
1136
|
+
// Chained comparisons: (< (< a b) c) → ((a < b) && (b < c)).
|
|
1137
|
+
// All COMPARE-level ops chain, including equality — otherwise
|
|
1138
|
+
// a == b < c silently compares a boolean to c. Recursing through
|
|
1139
|
+
// the left side handles chains of any length.
|
|
1140
|
+
let COMPARE_OPS = new Set(['<', '>', '<=', '>=', '==', '===', '!=', '!==']);
|
|
1141
|
+
if (COMPARE_OPS.has(op) && Array.isArray(left) && !left.parenthesized) {
|
|
1134
1142
|
let leftOp = left[0]?.valueOf?.() ?? left[0];
|
|
1135
1143
|
if (COMPARE_OPS.has(leftOp)) {
|
|
1136
|
-
let
|
|
1144
|
+
let jsOp = (o) => o === '==' ? '===' : o === '!=' ? '!==' : o;
|
|
1145
|
+
let leftCode = this.emit(left, 'value');
|
|
1137
1146
|
let b = this.emit(left[2], 'value');
|
|
1138
1147
|
let c = this.emit(right, 'value');
|
|
1139
|
-
return `(
|
|
1148
|
+
return `(${leftCode} && (${b} ${jsOp(op)} ${c}))`;
|
|
1140
1149
|
}
|
|
1141
1150
|
}
|
|
1142
1151
|
if (op === '==') op = '===';
|
|
1143
1152
|
if (op === '!=') op = '!==';
|
|
1153
|
+
// JS requires the base of ** to be an UpdateExpression: a bare unary
|
|
1154
|
+
// (typeof x ** 2, !a ** b, await f() ** 2) is a SyntaxError unless
|
|
1155
|
+
// parenthesized. Wrap unary-headed left operands that emit bare.
|
|
1156
|
+
if (op === '**' && Array.isArray(left)) {
|
|
1157
|
+
let UNARY_HEADS = new Set(['!', '~', 'typeof', 'void', 'delete', 'await', 'not', '+', '-']);
|
|
1158
|
+
let leftHead = left[0]?.valueOf?.() ?? left[0];
|
|
1159
|
+
if (UNARY_HEADS.has(leftHead)) {
|
|
1160
|
+
let lc = this.emit(left, 'value');
|
|
1161
|
+
if (!lc.startsWith('(')) lc = `(${lc})`;
|
|
1162
|
+
return `(${lc} ** ${this.emit(right, 'value')})`;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1144
1165
|
return `(${this.emit(left, 'value')} ${op} ${this.emit(right, 'value')})`;
|
|
1145
1166
|
}
|
|
1146
1167
|
|
|
@@ -1607,6 +1628,12 @@ export class CodeEmitter {
|
|
|
1607
1628
|
return `const ${str(name) ?? name} = ${this.emit(expr, 'value')}`;
|
|
1608
1629
|
}
|
|
1609
1630
|
|
|
1631
|
+
// The component macro consumes 'gate' statements before generator dispatch,
|
|
1632
|
+
// so reaching this generator means the binding sits outside a component body.
|
|
1633
|
+
emitGate(head, rest, context, sexpr) {
|
|
1634
|
+
this.error(`'<~' (render-ready gate) is only valid at the top of a component body`, sexpr);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1610
1637
|
emitEffect(head, rest) {
|
|
1611
1638
|
let [target, body] = rest;
|
|
1612
1639
|
this.usesReactivity = true;
|
|
@@ -1660,31 +1687,63 @@ export class CodeEmitter {
|
|
|
1660
1687
|
return `(${this.unwrap(this.emit(cond, 'value'))} ? ${this.emit(then_, 'value')} : ${this.emit(else_, 'value')})`;
|
|
1661
1688
|
}
|
|
1662
1689
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
let leftCode = this.emit(left, 'value');
|
|
1666
|
-
// Detect function calls: [fn, ...args] where fn is an identifier or accessor
|
|
1667
|
-
if (Array.isArray(right) && right.length > 1) {
|
|
1668
|
-
let fn = right[0];
|
|
1669
|
-
let isCall = Array.isArray(fn) || (typeof fn === 'string' && /^[a-zA-Z_$]/.test(fn));
|
|
1670
|
-
if (isCall) {
|
|
1671
|
-
let fnCode = this.emit(fn, 'value');
|
|
1672
|
-
let args = right.slice(1).map(a => this.emit(a, 'value'));
|
|
1673
|
-
return `${fnCode}(${leftCode}, ${args.join(', ')})`;
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
// Simple reference or property access — call with left as sole arg
|
|
1677
|
-
return `${this.emit(right, 'value')}(${leftCode})`;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
emitLoop(head, rest) {
|
|
1690
|
+
emitLoop(head, rest, context) {
|
|
1691
|
+
if (context === 'value') return this.emitLoopAsValue('while (true)', rest[0]);
|
|
1681
1692
|
return `while (true) ${this.emitLoopBody(rest[0])}`;
|
|
1682
1693
|
}
|
|
1683
1694
|
|
|
1684
|
-
emitLoopN(head, rest) {
|
|
1695
|
+
emitLoopN(head, rest, context) {
|
|
1685
1696
|
let [count, body] = rest;
|
|
1686
1697
|
let n = this.emit(count, 'value');
|
|
1687
|
-
|
|
1698
|
+
let header = `for (let it = 0; it < ${n}; it++)`;
|
|
1699
|
+
if (context === 'value') return this.emitLoopAsValue(header, body);
|
|
1700
|
+
return `${header} ${this.emitLoopBody(body)}`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Loops used as expressions collect each iteration's last body value into
|
|
1704
|
+
// an array (CoffeeScript semantics): x = (f() while c) → x = [f(), ...].
|
|
1705
|
+
// Mirrors emitForIn's value-context routing through comprehensions.
|
|
1706
|
+
emitLoopAsValue(header, body, guard) {
|
|
1707
|
+
let hasAwait = this.containsAwait(body) || (guard != null && this.containsAwait(guard));
|
|
1708
|
+
let code = this.asyncIIFEOpen(hasAwait) + '\n';
|
|
1709
|
+
this.indentLevel++;
|
|
1710
|
+
code += this.indent() + 'const result = [];\n';
|
|
1711
|
+
code += this.indent() + header + ' {\n';
|
|
1712
|
+
this.indentLevel++;
|
|
1713
|
+
if (guard != null) {
|
|
1714
|
+
code += this.indent() + `if (!(${this.unwrap(this.emit(guard, 'value'))})) continue;\n`;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
let stmts;
|
|
1718
|
+
if (!Array.isArray(body)) stmts = [body];
|
|
1719
|
+
else if (body[0] === 'block') stmts = body.slice(1);
|
|
1720
|
+
else if (Array.isArray(body[0])) stmts = body;
|
|
1721
|
+
else stmts = [body];
|
|
1722
|
+
|
|
1723
|
+
let loopStmts = ['for-in', 'for-of', 'for-as', 'while', 'loop'];
|
|
1724
|
+
let hasCtrl = (node) => {
|
|
1725
|
+
if (typeof node === 'string' && (node === 'break' || node === 'continue')) return true;
|
|
1726
|
+
if (!Array.isArray(node)) return false;
|
|
1727
|
+
if (['break', 'continue', 'return', 'throw'].includes(node[0])) return true;
|
|
1728
|
+
if (node[0] === 'if') return node.slice(1).some(hasCtrl);
|
|
1729
|
+
return node.some(hasCtrl);
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
1733
|
+
let s = stmts[i], isLast = i === stmts.length - 1;
|
|
1734
|
+
if (!isLast || hasCtrl(s) || (Array.isArray(s) && loopStmts.includes(s[0]))) {
|
|
1735
|
+
code += this.indent() + this.addSemicolon(s, this.emit(s, 'statement')) + '\n';
|
|
1736
|
+
} else {
|
|
1737
|
+
code += this.indent() + `result.push(${this.emit(s, 'value')});\n`;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
this.indentLevel--;
|
|
1742
|
+
code += this.indent() + '}\n';
|
|
1743
|
+
code += this.indent() + 'return result;\n';
|
|
1744
|
+
this.indentLevel--;
|
|
1745
|
+
code += this.indent() + '})()';
|
|
1746
|
+
return code;
|
|
1688
1747
|
}
|
|
1689
1748
|
|
|
1690
1749
|
emitAwait(head, rest) { return `await ${this.emit(rest[0], 'value')}`; }
|
|
@@ -1923,10 +1982,11 @@ export class CodeEmitter {
|
|
|
1923
1982
|
return code;
|
|
1924
1983
|
}
|
|
1925
1984
|
|
|
1926
|
-
emitWhile(head, rest) {
|
|
1985
|
+
emitWhile(head, rest, context) {
|
|
1927
1986
|
let cond = rest[0], guard = rest.length === 3 ? rest[1] : null, body = rest[rest.length - 1];
|
|
1928
|
-
let
|
|
1929
|
-
|
|
1987
|
+
let header = `while (${this.unwrap(this.emit(cond, 'value'))})`;
|
|
1988
|
+
if (context === 'value') return this.emitLoopAsValue(header, body, guard);
|
|
1989
|
+
return header + ' ' + (guard ? this.emitLoopBodyWithGuard(body, guard) : this.emitLoopBody(body));
|
|
1930
1990
|
}
|
|
1931
1991
|
|
|
1932
1992
|
emitRange(head, rest) {
|
|
@@ -4577,7 +4637,7 @@ export class Compiler {
|
|
|
4577
4637
|
// If only terminators remain (type-only source), emit types and return early
|
|
4578
4638
|
if (tokens.every(t => t[0] === 'TERMINATOR')) {
|
|
4579
4639
|
if (typeTokens && _typesEmitter) dts = _typesEmitter(typeTokens, ['program'], source);
|
|
4580
|
-
return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {} };
|
|
4640
|
+
return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {}, gates: [] };
|
|
4581
4641
|
}
|
|
4582
4642
|
|
|
4583
4643
|
// Step 3: Parse — shim adapter wraps token values with metadata
|
|
@@ -4656,6 +4716,8 @@ export class Compiler {
|
|
|
4656
4716
|
// the user might call any schema feature including .toSQL(). The browser
|
|
4657
4717
|
// bundle build script overrides to 'browser' for size reduction.
|
|
4658
4718
|
schemaMode: this.options.schemaMode,
|
|
4719
|
+
// Opt-in: fold derived projection schemas to self-contained literals.
|
|
4720
|
+
foldProjections: this.options.foldProjections,
|
|
4659
4721
|
sourceMap,
|
|
4660
4722
|
});
|
|
4661
4723
|
let code = generator.compile(sexpr);
|
|
@@ -4682,12 +4744,14 @@ export class Compiler {
|
|
|
4682
4744
|
code += `\n//# sourceMappingURL=${this.options.filename}.js.map`;
|
|
4683
4745
|
}
|
|
4684
4746
|
|
|
4685
|
-
// Step 5: Emit .d.ts from annotated tokens + parsed s-expression
|
|
4747
|
+
// Step 5: Emit .d.ts from annotated tokens + parsed s-expression. The
|
|
4748
|
+
// generator's `_schemaBehavior` buffer (populated during codegen, shadow-TS
|
|
4749
|
+
// mode only) lets the type emitter infer computed/derived return types.
|
|
4686
4750
|
if (typeTokens && _typesEmitter) {
|
|
4687
|
-
dts = _typesEmitter(typeTokens, sexpr, source);
|
|
4751
|
+
dts = _typesEmitter(typeTokens, sexpr, source, generator._schemaBehavior, generator._schemaAnon);
|
|
4688
4752
|
}
|
|
4689
4753
|
|
|
4690
|
-
return { tokens, sexpr, code, dts, map, reverseMap, data: dataSection, reactiveVars: generator.reactiveVars };
|
|
4754
|
+
return { tokens, sexpr, code, dts, map, reverseMap, data: dataSection, reactiveVars: generator.reactiveVars, gates: generator._gateDecls || [] };
|
|
4691
4755
|
}
|
|
4692
4756
|
|
|
4693
4757
|
compileToJS(source) { return this.compile(source).code; }
|
|
@@ -4754,6 +4818,126 @@ export function emit(sexpr, options = {}) {
|
|
|
4754
4818
|
return new CodeEmitter(options).compile(sexpr);
|
|
4755
4819
|
}
|
|
4756
4820
|
|
|
4821
|
+
// =============================================================================
|
|
4822
|
+
// Client projection extraction (browser bundle boundary)
|
|
4823
|
+
// =============================================================================
|
|
4824
|
+
//
|
|
4825
|
+
// When a browser-bundled module imports a binding from a server-only file
|
|
4826
|
+
// (e.g. `import { UserView } from '../api/models.rip'`), that file can't ship
|
|
4827
|
+
// to the browser — it carries the model's ORM/DDL. This lifts ONLY the named
|
|
4828
|
+
// bindings, and only when each is a self-contained schema, into a synthetic
|
|
4829
|
+
// shared module the browser can compile.
|
|
4830
|
+
//
|
|
4831
|
+
// The source is compiled with projection folding ON, so a derived schema
|
|
4832
|
+
// (`UserView = User.pick(...)`) becomes a source-free `__schema({...})` literal.
|
|
4833
|
+
// A binding is shippable iff it's such a literal AND not a :model AND carries
|
|
4834
|
+
// no behavior (methods/computed/transforms — which compile to functions that
|
|
4835
|
+
// could close over server-only imports). Anything else is refused with a
|
|
4836
|
+
// reason, keeping server code out of the browser by construction.
|
|
4837
|
+
//
|
|
4838
|
+
// Returns { ok: true, source } where `source` is synthetic .rip the browser can
|
|
4839
|
+
// compile, or { ok: false, error } listing every binding that can't be shipped.
|
|
4840
|
+
|
|
4841
|
+
function skipJsString(code, i, quote) {
|
|
4842
|
+
i++;
|
|
4843
|
+
while (i < code.length && code[i] !== quote) {
|
|
4844
|
+
if (code[i] === '\\') i += 2; else i++;
|
|
4845
|
+
}
|
|
4846
|
+
return i + 1;
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
// Locate a top-level `NAME = __schema(...)` binding in compiled JS and return
|
|
4850
|
+
// { text, isModel, hasBehavior } — or null when NAME isn't bound to a __schema
|
|
4851
|
+
// literal (an unfolded `Model.pick(x)` call, a function, a plain value, or
|
|
4852
|
+
// absent). Balanced extraction skips nested parens/brackets/braces and strings.
|
|
4853
|
+
function findSchemaLiteral(code, name) {
|
|
4854
|
+
// `name` is a schema identifier, but `$` is both a valid identifier char and
|
|
4855
|
+
// a regex metacharacter, so escape before interpolating into the pattern.
|
|
4856
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4857
|
+
const re = new RegExp(`(?:^|\\n)\\s*(?:export\\s+const\\s+)?${escaped}\\s*=\\s*__schema\\(`, 'g');
|
|
4858
|
+
const m = re.exec(code);
|
|
4859
|
+
if (!m) return null;
|
|
4860
|
+
let i = m.index + m[0].length; // just past the opening '('
|
|
4861
|
+
const start = i;
|
|
4862
|
+
let depth = 1;
|
|
4863
|
+
while (i < code.length && depth > 0) {
|
|
4864
|
+
const c = code[i];
|
|
4865
|
+
if (c === '"' || c === "'" || c === '`') { i = skipJsString(code, i, c); continue; }
|
|
4866
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
4867
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
4868
|
+
i++;
|
|
4869
|
+
}
|
|
4870
|
+
if (depth !== 0) return null;
|
|
4871
|
+
const argText = code.slice(start, i - 1);
|
|
4872
|
+
// Behavior is detected structurally, not by scanning for the word "function":
|
|
4873
|
+
// a callable entry surfaces as `tag: "computed"|...`, and a field transform as
|
|
4874
|
+
// `transform: (function…)`. A bare `\bfunction\b` would also match a literal
|
|
4875
|
+
// VALUE like `kind! "function" | "class"` and wrongly refuse a plain shape.
|
|
4876
|
+
return {
|
|
4877
|
+
text: `__schema(${argText})`,
|
|
4878
|
+
isModel: /^\s*\{\s*kind:\s*"model"/.test(argText),
|
|
4879
|
+
hasBehavior: /\btransform:/.test(argText) || /tag:\s*"(?:computed|method|hook|ensure|derived)"/.test(argText),
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
export function extractClientProjections(source, names, { filename } = {}) {
|
|
4884
|
+
let compiled;
|
|
4885
|
+
try {
|
|
4886
|
+
compiled = new Compiler({
|
|
4887
|
+
filename,
|
|
4888
|
+
foldProjections: true,
|
|
4889
|
+
skipPreamble: true,
|
|
4890
|
+
skipRuntimes: true,
|
|
4891
|
+
skipDataPart: true,
|
|
4892
|
+
skipImports: true,
|
|
4893
|
+
}).compile(source);
|
|
4894
|
+
} catch (e) {
|
|
4895
|
+
return { ok: false, error: `failed to compile ${filename || 'module'}: ${e.message}` };
|
|
4896
|
+
}
|
|
4897
|
+
const code = compiled.code || '';
|
|
4898
|
+
// Every schema bound in this source — lets us tell a nested *schema-typed*
|
|
4899
|
+
// field (`items! OrderItem[]` → `typeName: "OrderItem"`) apart from a
|
|
4900
|
+
// primitive (`typeName: "string"`).
|
|
4901
|
+
const defined = new Set();
|
|
4902
|
+
for (const m of code.matchAll(/(?:^|\n)\s*(?:export\s+const\s+)?(\w+)\s*=\s*__schema\(/g)) defined.add(m[1]);
|
|
4903
|
+
|
|
4904
|
+
const lines = [];
|
|
4905
|
+
const errors = [];
|
|
4906
|
+
const where = filename ? ` from '${filename}'` : '';
|
|
4907
|
+
const requested = new Set(names);
|
|
4908
|
+
const seen = new Set();
|
|
4909
|
+
// Process requested names, then transitively any schema they reference by a
|
|
4910
|
+
// field type — otherwise the shared module ships a shape whose nested type
|
|
4911
|
+
// isn't registered in the browser, and that field's validation is *silently*
|
|
4912
|
+
// skipped. A pulled-in dependency that isn't itself shippable is a hard error.
|
|
4913
|
+
const queue = [...names];
|
|
4914
|
+
while (queue.length) {
|
|
4915
|
+
const name = queue.shift();
|
|
4916
|
+
if (seen.has(name)) continue;
|
|
4917
|
+
seen.add(name);
|
|
4918
|
+
const lit = findSchemaLiteral(code, name);
|
|
4919
|
+
const isDep = !requested.has(name);
|
|
4920
|
+
const tag = isDep ? `'${name}'${where} (a nested type of a shipped projection)` : `'${name}'${where}`;
|
|
4921
|
+
if (!lit) {
|
|
4922
|
+
errors.push(isDep
|
|
4923
|
+
? `${tag} isn't a shippable schema, so the projection referencing it can't cross to the browser`
|
|
4924
|
+
: `${tag} isn't a shippable schema — only a schema or a folded projection (e.g. ${name} = Model.pick(...)) can cross to the browser`);
|
|
4925
|
+
} else if (lit.isModel) {
|
|
4926
|
+
errors.push(`${tag} is a :model — ship a projection like ${name}.pick(...)/.omit(...) instead of the model (which carries ORM/DDL)`);
|
|
4927
|
+
} else if (lit.hasBehavior) {
|
|
4928
|
+
errors.push(`${tag} carries behavior (methods/computed/transforms) that may close over server-only code — project it to plain fields before importing it client-side`);
|
|
4929
|
+
} else {
|
|
4930
|
+
lines.push(`export ${name} = ${lit.text}`);
|
|
4931
|
+
for (const tm of lit.text.matchAll(/typeName:\s*"(\w+)"/g)) {
|
|
4932
|
+
const dep = tm[1];
|
|
4933
|
+
if (defined.has(dep) && !seen.has(dep)) queue.push(dep);
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
if (errors.length) return { ok: false, error: errors.join('; ') };
|
|
4938
|
+
return { ok: true, source: lines.join('\n') + '\n' };
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4757
4941
|
export function getReactiveRuntime() {
|
|
4758
4942
|
return new CodeEmitter({}).getReactiveRuntime();
|
|
4759
4943
|
}
|