rip-lang 3.16.0 → 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 +3 -4
- package/bin/rip +201 -18
- package/bin/rip-schema +175 -0
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +200 -19
- package/docs/RIP-DUCKDB.md +64 -1
- package/docs/RIP-INTRO.md +4 -4
- package/docs/RIP-LANG.md +36 -38
- package/docs/RIP-SCHEMA.md +1204 -364
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +4195 -966
- package/docs/dist/rip.min.js +1161 -284
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +2 -1
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- 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/bundle.json +55 -55
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +1 -1
- package/docs/ui/index.html +1 -1
- package/package.json +15 -7
- package/rip-loader.js +59 -2
- package/src/AGENTS.md +43 -12
- package/src/browser.js +52 -11
- package/src/compiler.js +538 -80
- package/src/components.js +488 -48
- package/src/dts.js +80 -50
- 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 +82 -32
- 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 +1054 -94
- package/src/typecheck.js +1610 -127
- package/src/types.js +90 -6
- package/src/grammar/lunar.rip +0 -2412
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
package/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
|
|
|
@@ -322,6 +327,20 @@ export class CodeEmitter {
|
|
|
322
327
|
// Each entry pairs a statement's generated code with its source loc.
|
|
323
328
|
// Output line positions are computed by exact arithmetic — no heuristics.
|
|
324
329
|
buildMappings() {
|
|
330
|
+
// Imports are emitted at the top of the file (before the preamble),
|
|
331
|
+
// starting at line 0. Process them first with their own line offset.
|
|
332
|
+
if (this._importEntries) {
|
|
333
|
+
let importLineOffset = 0;
|
|
334
|
+
for (let entry of this._importEntries) {
|
|
335
|
+
if (entry.loc) {
|
|
336
|
+
this.sourceMap.addMapping(importLineOffset, 0, entry.loc.r, entry.loc.c);
|
|
337
|
+
}
|
|
338
|
+
if (entry.sexpr && entry.loc) {
|
|
339
|
+
this.recordSubMappings(entry.code, entry.sexpr, importLineOffset);
|
|
340
|
+
}
|
|
341
|
+
importLineOffset += entry.code.split('\n').length;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
325
344
|
if (!this._stmtEntries) return;
|
|
326
345
|
let lineOffset = this._preambleLines;
|
|
327
346
|
for (let entry of this._stmtEntries) {
|
|
@@ -423,6 +442,47 @@ export class CodeEmitter {
|
|
|
423
442
|
ripSrcCache.set(genLineInStmt, v);
|
|
424
443
|
return v;
|
|
425
444
|
};
|
|
445
|
+
// Inline type annotations (emitted when `inlineTypes: true`) inject
|
|
446
|
+
// identifiers into function-signature lines that have no source
|
|
447
|
+
// counterpart — e.g. `header(name, value, opts: { append?: boolean })`.
|
|
448
|
+
// Their identifiers (`append`, `boolean`, etc.) would otherwise compete
|
|
449
|
+
// with real body identifiers in the regex matcher below, mis-mapping
|
|
450
|
+
// source positions onto the type literal. Detect those brace ranges
|
|
451
|
+
// per line and skip matches inside them.
|
|
452
|
+
const inlineTypeRangesCache = new Map();
|
|
453
|
+
const getInlineTypeRanges = (genLineInStmt) => {
|
|
454
|
+
if (inlineTypeRangesCache.has(genLineInStmt)) return inlineTypeRangesCache.get(genLineInStmt);
|
|
455
|
+
const lt = codeLines[genLineInStmt];
|
|
456
|
+
const ranges = [];
|
|
457
|
+
// Only look at function-signature shaped lines: contain `(...) {` or `(...) =>`.
|
|
458
|
+
if (lt && /\)\s*(\{|=>)\s*$/.test(lt)) {
|
|
459
|
+
// Find `: {` after an identifier (with optional `?` and whitespace).
|
|
460
|
+
const annotRe = /\b[a-zA-Z_$][\w$]*\??\s*:\s*\{/g;
|
|
461
|
+
let am;
|
|
462
|
+
while ((am = annotRe.exec(lt)) !== null) {
|
|
463
|
+
const braceStart = am.index + am[0].length - 1; // position of `{`
|
|
464
|
+
// Skip inside strings (defensive — unlikely here)
|
|
465
|
+
if (CodeEmitter._isColInsideString(lt, braceStart)) continue;
|
|
466
|
+
// Walk to matching `}` honoring brace depth and strings
|
|
467
|
+
let depth = 1, j = braceStart + 1, inStr = false, quote = '';
|
|
468
|
+
while (j < lt.length && depth > 0) {
|
|
469
|
+
const ch = lt[j];
|
|
470
|
+
if (inStr) {
|
|
471
|
+
if (ch === '\\') { j += 2; continue; }
|
|
472
|
+
if (ch === quote) inStr = false;
|
|
473
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
474
|
+
inStr = true; quote = ch;
|
|
475
|
+
} else if (ch === '{') depth++;
|
|
476
|
+
else if (ch === '}') depth--;
|
|
477
|
+
j++;
|
|
478
|
+
}
|
|
479
|
+
ranges.push([braceStart, j]); // exclude positions [braceStart, j)
|
|
480
|
+
annotRe.lastIndex = j;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
inlineTypeRangesCache.set(genLineInStmt, ranges);
|
|
484
|
+
return ranges;
|
|
485
|
+
};
|
|
426
486
|
for (let { name, origLine, origCol } of subs) {
|
|
427
487
|
let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
428
488
|
let re = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
@@ -442,6 +502,11 @@ export class CodeEmitter {
|
|
|
442
502
|
const annotSrc = getRipSrcAnnot(genLineInStmt);
|
|
443
503
|
const annotMatches = annotSrc != null && annotSrc === origLine;
|
|
444
504
|
if (lineText && CodeEmitter._isColInsideString(lineText, genCol) && !annotMatches) continue;
|
|
505
|
+
// Skip matches inside inline type annotation brace ranges (e.g.
|
|
506
|
+
// `opts: { append?: boolean }`) — those identifiers are emitted
|
|
507
|
+
// for TS type-checking only and have no real source counterpart.
|
|
508
|
+
const itRanges = getInlineTypeRanges(genLineInStmt);
|
|
509
|
+
if (itRanges.length && itRanges.some(([s, e]) => genCol >= s && genCol < e)) continue;
|
|
445
510
|
let genLine = lineOffset + genLineInStmt;
|
|
446
511
|
// Annotation-matched lines are the authoritative gen position for
|
|
447
512
|
// their source line — score them as a perfect line match so they
|
|
@@ -501,14 +566,37 @@ export class CodeEmitter {
|
|
|
501
566
|
}
|
|
502
567
|
}
|
|
503
568
|
// Operators/keywords: anchor is the subject at index 1
|
|
504
|
-
else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]
|
|
505
|
-
if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1]))
|
|
569
|
+
else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]|^\.{1,3}$|^def$|^class$|^state$|^computed$|^readonly$|^for-/.test(head)) {
|
|
570
|
+
if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1])) {
|
|
571
|
+
ident = node[1];
|
|
572
|
+
// Spread `...x`: node.loc.c marks the `...` start; shift past the
|
|
573
|
+
// operator so the anchor lands on the operand identifier.
|
|
574
|
+
if (head === '...') identCol = node.loc.c + 3;
|
|
575
|
+
}
|
|
506
576
|
}
|
|
507
577
|
// Function call (head is identifier)
|
|
508
578
|
else if (typeof head === 'string' && /^[a-zA-Z_$]/.test(head)) {
|
|
509
579
|
ident = head;
|
|
510
580
|
}
|
|
511
581
|
if (ident) result.push({ name: ident, origLine: node.loc.r, origCol: identCol });
|
|
582
|
+
|
|
583
|
+
// Arrow body bare-identifier anchor: a single-expression arrow body
|
|
584
|
+
// like `-> products` parses as `['->', [], ['block', 'products']]`.
|
|
585
|
+
// The body atom has no .loc (parser only attaches loc to arrays), and
|
|
586
|
+
// the `block` wrapper has bogus `loc=0:0`, so the identifier reference
|
|
587
|
+
// is invisible to the heuristic mapping. Synthesize an anchor by
|
|
588
|
+
// scanning source forward from the arrow's location.
|
|
589
|
+
if ((head === '->' || head === '=>') && Array.isArray(node[2]) && str(node[2][0]) === 'block') {
|
|
590
|
+
const body = node[2];
|
|
591
|
+
for (let i = 1; i < body.length; i++) {
|
|
592
|
+
const leaf = body[i];
|
|
593
|
+
const leafStr = typeof leaf === 'string' || leaf instanceof String ? str(leaf) : null;
|
|
594
|
+
if (leafStr && /^[a-zA-Z_$][\w$]*$/.test(leafStr) && !leaf.loc) {
|
|
595
|
+
const anchor = this._scanForIdentAfter(leafStr, node.loc);
|
|
596
|
+
if (anchor) result.push(anchor);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
512
600
|
}
|
|
513
601
|
// Side-channel anchors attached by walkRender for bare-identifier
|
|
514
602
|
// children of template-tag nodes (e.g. `error` in `p.error error`).
|
|
@@ -528,6 +616,30 @@ export class CodeEmitter {
|
|
|
528
616
|
}
|
|
529
617
|
}
|
|
530
618
|
|
|
619
|
+
// Scan original source for the first occurrence of `ident` after the
|
|
620
|
+
// given start location (typically an arrow's `->` loc). Skips matches
|
|
621
|
+
// inside string/comment regions. Returns a sub-expression anchor or null.
|
|
622
|
+
_scanForIdentAfter(ident, startLoc) {
|
|
623
|
+
const source = this.options && this.options.source;
|
|
624
|
+
if (!source || !startLoc) return null;
|
|
625
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
626
|
+
const re = new RegExp('\\b' + ident.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
|
|
627
|
+
const startRow = startLoc.r;
|
|
628
|
+
const startCol = startLoc.c;
|
|
629
|
+
// Search current line from startCol forward, then up to 20 subsequent lines.
|
|
630
|
+
for (let r = startRow; r < Math.min(lines.length, startRow + 20); r++) {
|
|
631
|
+
const line = lines[r];
|
|
632
|
+
if (!line) continue;
|
|
633
|
+
re.lastIndex = r === startRow ? startCol : 0;
|
|
634
|
+
let m;
|
|
635
|
+
while ((m = re.exec(line)) !== null) {
|
|
636
|
+
if (CodeEmitter._isColInsideString(line, m.index)) continue;
|
|
637
|
+
return { name: ident, origLine: r, origCol: m.index };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
531
643
|
// ---------------------------------------------------------------------------
|
|
532
644
|
// Variable collection
|
|
533
645
|
// ---------------------------------------------------------------------------
|
|
@@ -577,7 +689,12 @@ export class CodeEmitter {
|
|
|
577
689
|
}
|
|
578
690
|
|
|
579
691
|
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
580
|
-
|
|
692
|
+
// Don't hoist loop vars: emitForIn/emitForOf/emitForAs already
|
|
693
|
+
// emit `for (let x of ...)`, which is block-scoped AND gives JS's
|
|
694
|
+
// per-iteration binding semantics (critical for closures captured
|
|
695
|
+
// inside the loop). Hoisting `let x` to the surrounding scope
|
|
696
|
+
// would shadow that as dead code (TS6133 "declared but never
|
|
697
|
+
// read") without changing runtime behavior.
|
|
581
698
|
rest.slice(1).forEach(item => this.collectProgramVariables(item));
|
|
582
699
|
return;
|
|
583
700
|
}
|
|
@@ -640,7 +757,10 @@ export class CodeEmitter {
|
|
|
640
757
|
return;
|
|
641
758
|
}
|
|
642
759
|
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
643
|
-
|
|
760
|
+
// See collectProgramVariables for-loop branch: loop vars are
|
|
761
|
+
// block-scoped via the for-header's `let`; hoisting them here
|
|
762
|
+
// would only produce a redundant outer `let x` flagged as
|
|
763
|
+
// unused.
|
|
644
764
|
rest.slice(1).forEach(collect);
|
|
645
765
|
return;
|
|
646
766
|
}
|
|
@@ -668,20 +788,55 @@ export class CodeEmitter {
|
|
|
668
788
|
return vars;
|
|
669
789
|
}
|
|
670
790
|
|
|
791
|
+
// Walk a function body and collect typed local assignments. Returns a
|
|
792
|
+
// Map<name, typeString> for every `name:: T = value` whose target is a
|
|
793
|
+
// String-wrapped identifier carrying `data.type` (attached by types.js).
|
|
794
|
+
//
|
|
795
|
+
// Used by `emitBodyWithReturns` in `inlineTypes` mode to annotate the
|
|
796
|
+
// function-top hoist (`let a, y: boolean, b;`) so shadow-TS sees the
|
|
797
|
+
// intended type instead of inferring a literal from the first RHS. Stops
|
|
798
|
+
// at nested function boundaries — each function owns its own typed locals.
|
|
799
|
+
//
|
|
800
|
+
// Conflict policy: first annotation wins. Same-name re-annotations are
|
|
801
|
+
// silently ignored; mixing different types on the same local is an
|
|
802
|
+
// unusual pattern best surfaced by TS itself once the hoist carries the
|
|
803
|
+
// first annotation.
|
|
804
|
+
collectTypedLocals(body) {
|
|
805
|
+
let typed = new Map();
|
|
806
|
+
let walk = (sexpr) => {
|
|
807
|
+
if (!Array.isArray(sexpr)) return;
|
|
808
|
+
let [head, ...rest] = sexpr;
|
|
809
|
+
head = str(head);
|
|
810
|
+
if (Array.isArray(head)) { sexpr.forEach(walk); return; }
|
|
811
|
+
if (CodeEmitter.ASSIGNMENT_OPS.has(head)) {
|
|
812
|
+
let [target, value] = rest;
|
|
813
|
+
if (target instanceof String && target.type && !typed.has(str(target))) {
|
|
814
|
+
typed.set(str(target), target.type);
|
|
815
|
+
}
|
|
816
|
+
walk(value);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (head === 'def' || head === '->' || head === '=>' || head === 'effect') return;
|
|
820
|
+
rest.forEach(walk);
|
|
821
|
+
};
|
|
822
|
+
walk(body);
|
|
823
|
+
return typed;
|
|
824
|
+
}
|
|
825
|
+
|
|
671
826
|
// ---------------------------------------------------------------------------
|
|
672
827
|
// Main dispatch
|
|
673
828
|
// ---------------------------------------------------------------------------
|
|
674
829
|
|
|
675
830
|
emit(sexpr, context = 'statement') {
|
|
676
|
-
// String object with metadata (quote,
|
|
831
|
+
// String object with metadata (quote, bang, optional, heregex, etc.)
|
|
677
832
|
if (sexpr instanceof String) {
|
|
678
833
|
// Dammit operator (!)
|
|
679
|
-
if (meta(sexpr, '
|
|
834
|
+
if (meta(sexpr, 'bang') === true) {
|
|
680
835
|
return `await ${str(sexpr)}()`;
|
|
681
836
|
}
|
|
682
837
|
|
|
683
838
|
// Existence check (?)
|
|
684
|
-
if (meta(sexpr, '
|
|
839
|
+
if (meta(sexpr, 'optional')) {
|
|
685
840
|
return `(${str(sexpr)} != null)`;
|
|
686
841
|
}
|
|
687
842
|
|
|
@@ -737,8 +892,8 @@ export class CodeEmitter {
|
|
|
737
892
|
|
|
738
893
|
let [head, ...rest] = sexpr;
|
|
739
894
|
|
|
740
|
-
// Preserve
|
|
741
|
-
let
|
|
895
|
+
// Preserve bang metadata before converting head to primitive
|
|
896
|
+
let headBangMeta = meta(head, 'bang');
|
|
742
897
|
head = str(head);
|
|
743
898
|
|
|
744
899
|
// Dispatch table
|
|
@@ -758,7 +913,7 @@ export class CodeEmitter {
|
|
|
758
913
|
let postfix = this._tryPostfixCall(head, rest, context);
|
|
759
914
|
if (postfix) return postfix;
|
|
760
915
|
|
|
761
|
-
let needsAwait =
|
|
916
|
+
let needsAwait = headBangMeta === true;
|
|
762
917
|
let callStr = `${this.emit(head, 'value')}(${this._emitArgs(rest)})`;
|
|
763
918
|
return needsAwait ? `await ${callStr}` : callStr;
|
|
764
919
|
}
|
|
@@ -784,15 +939,16 @@ export class CodeEmitter {
|
|
|
784
939
|
let postfix = this._tryPostfixCall(head, rest, context);
|
|
785
940
|
if (postfix) return postfix;
|
|
786
941
|
|
|
787
|
-
// Property access with
|
|
942
|
+
// Property access with bang sigil on property
|
|
788
943
|
let needsAwait = false;
|
|
789
944
|
let calleeCode;
|
|
790
|
-
if (head[0] === '.' && meta(head[2], '
|
|
945
|
+
if (head[0] === '.' && meta(head[2], 'bang') === true) {
|
|
791
946
|
needsAwait = true;
|
|
792
947
|
let [obj, prop] = head.slice(1);
|
|
793
948
|
let objCode = this.emit(obj, 'value');
|
|
794
949
|
let needsParens = CodeEmitter.NUMBER_LITERAL_RE.test(objCode) ||
|
|
795
|
-
((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield')))
|
|
950
|
+
((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield'))) ||
|
|
951
|
+
/^(await|yield)\s/.test(objCode);
|
|
796
952
|
let base = needsParens ? `(${objCode})` : objCode;
|
|
797
953
|
calleeCode = `${base}.${str(prop)}`;
|
|
798
954
|
} else {
|
|
@@ -850,7 +1006,12 @@ export class CodeEmitter {
|
|
|
850
1006
|
let needsBlank = false;
|
|
851
1007
|
|
|
852
1008
|
if (imports.length > 0) {
|
|
853
|
-
|
|
1009
|
+
let importEntries = imports.map(s => {
|
|
1010
|
+
let generated = this.addSemicolon(s, this.emit(s, 'statement'));
|
|
1011
|
+
return { code: generated, loc: Array.isArray(s) ? s.loc : null, sexpr: Array.isArray(s) ? s : null };
|
|
1012
|
+
});
|
|
1013
|
+
this._importEntries = importEntries;
|
|
1014
|
+
code += importEntries.map(e => e.code).join('\n');
|
|
854
1015
|
needsBlank = true;
|
|
855
1016
|
}
|
|
856
1017
|
|
|
@@ -919,9 +1080,9 @@ export class CodeEmitter {
|
|
|
919
1080
|
|
|
920
1081
|
if (this.usesTemplates && !skip) {
|
|
921
1082
|
if (skipRT) {
|
|
922
|
-
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';
|
|
923
1084
|
} else if (typeof globalThis !== 'undefined' && globalThis.__ripComponent) {
|
|
924
|
-
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';
|
|
925
1086
|
} else {
|
|
926
1087
|
code += this.getComponentRuntime();
|
|
927
1088
|
}
|
|
@@ -972,19 +1133,35 @@ export class CodeEmitter {
|
|
|
972
1133
|
return `${this.emit(left, 'value')}.repeat(${this.emit(right, 'value')})`;
|
|
973
1134
|
}
|
|
974
1135
|
}
|
|
975
|
-
// Chained comparisons: (< (< a b) c) → ((a < b) && (b < c))
|
|
976
|
-
|
|
977
|
-
|
|
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) {
|
|
978
1142
|
let leftOp = left[0]?.valueOf?.() ?? left[0];
|
|
979
1143
|
if (COMPARE_OPS.has(leftOp)) {
|
|
980
|
-
let
|
|
1144
|
+
let jsOp = (o) => o === '==' ? '===' : o === '!=' ? '!==' : o;
|
|
1145
|
+
let leftCode = this.emit(left, 'value');
|
|
981
1146
|
let b = this.emit(left[2], 'value');
|
|
982
1147
|
let c = this.emit(right, 'value');
|
|
983
|
-
return `(
|
|
1148
|
+
return `(${leftCode} && (${b} ${jsOp(op)} ${c}))`;
|
|
984
1149
|
}
|
|
985
1150
|
}
|
|
986
1151
|
if (op === '==') op = '===';
|
|
987
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
|
+
}
|
|
988
1165
|
return `(${this.emit(left, 'value')} ${op} ${this.emit(right, 'value')})`;
|
|
989
1166
|
}
|
|
990
1167
|
|
|
@@ -1042,15 +1219,7 @@ export class CodeEmitter {
|
|
|
1042
1219
|
}
|
|
1043
1220
|
|
|
1044
1221
|
// Validate: no sigils in assignment targets (except void function syntax)
|
|
1045
|
-
|
|
1046
|
-
if (target instanceof String && meta(target, 'await') !== undefined && !isFnValue) {
|
|
1047
|
-
let sigil = meta(target, 'await') === true ? '!' : '&';
|
|
1048
|
-
this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (target instanceof String && meta(target, 'await') === true && isFnValue) {
|
|
1052
|
-
this.nextFunctionIsVoid = true;
|
|
1053
|
-
}
|
|
1222
|
+
this.applyVoidMarker(target, value, sexpr);
|
|
1054
1223
|
|
|
1055
1224
|
// Empty destructuring — just evaluate RHS
|
|
1056
1225
|
let isEmptyArr = this.is(target, 'array', 0);
|
|
@@ -1139,7 +1308,7 @@ export class CodeEmitter {
|
|
|
1139
1308
|
|
|
1140
1309
|
// Generate target (handle reactive, sigils)
|
|
1141
1310
|
let targetCode;
|
|
1142
|
-
if (target instanceof String && meta(target, '
|
|
1311
|
+
if (target instanceof String && meta(target, 'bang') !== undefined) {
|
|
1143
1312
|
targetCode = str(target);
|
|
1144
1313
|
} else if (typeof target === 'string' && this.reactiveVars?.has(target)) {
|
|
1145
1314
|
targetCode = `${target}.value`;
|
|
@@ -1186,8 +1355,8 @@ export class CodeEmitter {
|
|
|
1186
1355
|
objCode.startsWith('await ') ||
|
|
1187
1356
|
((this.is(obj, 'object') || this.is(obj, 'yield')));
|
|
1188
1357
|
let base = needsParens ? `(${objCode})` : objCode;
|
|
1189
|
-
if (meta(prop, '
|
|
1190
|
-
if (meta(prop, '
|
|
1358
|
+
if (meta(prop, 'bang') === true) return `await ${base}.${str(prop)}()`;
|
|
1359
|
+
if (meta(prop, 'optional')) return `(${base}.${str(prop)} != null)`;
|
|
1191
1360
|
return `${base}.${str(prop)}`;
|
|
1192
1361
|
}
|
|
1193
1362
|
|
|
@@ -1353,7 +1522,7 @@ export class CodeEmitter {
|
|
|
1353
1522
|
|
|
1354
1523
|
emitDef(head, rest, context, sexpr) {
|
|
1355
1524
|
let [name, params, body] = rest;
|
|
1356
|
-
let sideEffectOnly = meta(name, '
|
|
1525
|
+
let sideEffectOnly = meta(name, 'bang') === true;
|
|
1357
1526
|
let cleanName = str(name);
|
|
1358
1527
|
let paramList = this.emitParamList(params);
|
|
1359
1528
|
let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
|
|
@@ -1365,8 +1534,7 @@ export class CodeEmitter {
|
|
|
1365
1534
|
emitThinArrow(head, rest, context, sexpr) {
|
|
1366
1535
|
let [params, body] = rest;
|
|
1367
1536
|
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
|
|
1368
|
-
let sideEffectOnly =
|
|
1369
|
-
this.nextFunctionIsVoid = false;
|
|
1537
|
+
let sideEffectOnly = sexpr.isVoid || false;
|
|
1370
1538
|
let paramList = this.emitParamList(params);
|
|
1371
1539
|
let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
|
|
1372
1540
|
let isAsync = this.containsAwait(body);
|
|
@@ -1378,8 +1546,7 @@ export class CodeEmitter {
|
|
|
1378
1546
|
emitFatArrow(head, rest, context, sexpr) {
|
|
1379
1547
|
let [params, body] = rest;
|
|
1380
1548
|
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
|
|
1381
|
-
let sideEffectOnly =
|
|
1382
|
-
this.nextFunctionIsVoid = false;
|
|
1549
|
+
let sideEffectOnly = sexpr.isVoid || false;
|
|
1383
1550
|
let paramList = this.emitParamList(params);
|
|
1384
1551
|
let isSingle = params.length === 1 && typeof params[0] === 'string' &&
|
|
1385
1552
|
!paramList.includes('=') && !paramList.includes('...') &&
|
|
@@ -1461,6 +1628,12 @@ export class CodeEmitter {
|
|
|
1461
1628
|
return `const ${str(name) ?? name} = ${this.emit(expr, 'value')}`;
|
|
1462
1629
|
}
|
|
1463
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
|
+
|
|
1464
1637
|
emitEffect(head, rest) {
|
|
1465
1638
|
let [target, body] = rest;
|
|
1466
1639
|
this.usesReactivity = true;
|
|
@@ -1514,31 +1687,63 @@ export class CodeEmitter {
|
|
|
1514
1687
|
return `(${this.unwrap(this.emit(cond, 'value'))} ? ${this.emit(then_, 'value')} : ${this.emit(else_, 'value')})`;
|
|
1515
1688
|
}
|
|
1516
1689
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
let leftCode = this.emit(left, 'value');
|
|
1520
|
-
// Detect function calls: [fn, ...args] where fn is an identifier or accessor
|
|
1521
|
-
if (Array.isArray(right) && right.length > 1) {
|
|
1522
|
-
let fn = right[0];
|
|
1523
|
-
let isCall = Array.isArray(fn) || (typeof fn === 'string' && /^[a-zA-Z_$]/.test(fn));
|
|
1524
|
-
if (isCall) {
|
|
1525
|
-
let fnCode = this.emit(fn, 'value');
|
|
1526
|
-
let args = right.slice(1).map(a => this.emit(a, 'value'));
|
|
1527
|
-
return `${fnCode}(${leftCode}, ${args.join(', ')})`;
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
// Simple reference or property access — call with left as sole arg
|
|
1531
|
-
return `${this.emit(right, 'value')}(${leftCode})`;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
emitLoop(head, rest) {
|
|
1690
|
+
emitLoop(head, rest, context) {
|
|
1691
|
+
if (context === 'value') return this.emitLoopAsValue('while (true)', rest[0]);
|
|
1535
1692
|
return `while (true) ${this.emitLoopBody(rest[0])}`;
|
|
1536
1693
|
}
|
|
1537
1694
|
|
|
1538
|
-
emitLoopN(head, rest) {
|
|
1695
|
+
emitLoopN(head, rest, context) {
|
|
1539
1696
|
let [count, body] = rest;
|
|
1540
1697
|
let n = this.emit(count, 'value');
|
|
1541
|
-
|
|
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;
|
|
1542
1747
|
}
|
|
1543
1748
|
|
|
1544
1749
|
emitAwait(head, rest) { return `await ${this.emit(rest[0], 'value')}`; }
|
|
@@ -1777,10 +1982,11 @@ export class CodeEmitter {
|
|
|
1777
1982
|
return code;
|
|
1778
1983
|
}
|
|
1779
1984
|
|
|
1780
|
-
emitWhile(head, rest) {
|
|
1985
|
+
emitWhile(head, rest, context) {
|
|
1781
1986
|
let cond = rest[0], guard = rest.length === 3 ? rest[1] : null, body = rest[rest.length - 1];
|
|
1782
|
-
let
|
|
1783
|
-
|
|
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));
|
|
1784
1990
|
}
|
|
1785
1991
|
|
|
1786
1992
|
emitRange(head, rest) {
|
|
@@ -1896,7 +2102,20 @@ export class CodeEmitter {
|
|
|
1896
2102
|
// Symbol literals
|
|
1897
2103
|
// ---------------------------------------------------------------------------
|
|
1898
2104
|
|
|
1899
|
-
emitSymbol(head, rest
|
|
2105
|
+
emitSymbol(head, rest, context, sexpr) {
|
|
2106
|
+
// Anchor the symbol name's source position to the generated `Symbol`
|
|
2107
|
+
// identifier so hover on `:foo` shows `SymbolConstructor` (the
|
|
2108
|
+
// generated `"foo"` is a string literal that TS has no hover for).
|
|
2109
|
+
// sexpr.loc points at the `:`; the name starts at col + 1.
|
|
2110
|
+
if (sexpr && sexpr.loc && typeof rest[0] === 'string') {
|
|
2111
|
+
sexpr._anchors = (sexpr._anchors || []).concat([{
|
|
2112
|
+
name: 'Symbol',
|
|
2113
|
+
origLine: sexpr.loc.r,
|
|
2114
|
+
origCol: sexpr.loc.c + 1,
|
|
2115
|
+
}]);
|
|
2116
|
+
}
|
|
2117
|
+
return `Symbol.for(${JSON.stringify(rest[0])})`;
|
|
2118
|
+
}
|
|
1900
2119
|
|
|
1901
2120
|
// ---------------------------------------------------------------------------
|
|
1902
2121
|
// Data structures
|
|
@@ -1996,8 +2215,7 @@ export class CodeEmitter {
|
|
|
1996
2215
|
if (operator === ':' && isSimpleKey && this.is(value, '->')) {
|
|
1997
2216
|
let [, mParams, mBody] = value;
|
|
1998
2217
|
if ((!mParams || (Array.isArray(mParams) && mParams.length === 0)) && this.containsIt(mBody)) mParams = ['it'];
|
|
1999
|
-
let mSideEffect =
|
|
2000
|
-
this.nextFunctionIsVoid = false;
|
|
2218
|
+
let mSideEffect = value.isVoid || false;
|
|
2001
2219
|
let mParamList = this.emitParamList(mParams);
|
|
2002
2220
|
let mBodyCode = this.emitFunctionBody(mBody, mParams, mSideEffect);
|
|
2003
2221
|
let mIsAsync = this.containsAwait(mBody);
|
|
@@ -2578,7 +2796,7 @@ export class CodeEmitter {
|
|
|
2578
2796
|
emitImport(head, rest, context, sexpr) {
|
|
2579
2797
|
if (rest.length === 1) {
|
|
2580
2798
|
let importExpr = `import(${this.emit(rest[0], 'value')})`;
|
|
2581
|
-
if (meta(sexpr[0], '
|
|
2799
|
+
if (meta(sexpr[0], 'bang') === true) return `(await ${importExpr})`;
|
|
2582
2800
|
return importExpr;
|
|
2583
2801
|
}
|
|
2584
2802
|
if (this.options.skipImports) return '';
|
|
@@ -2587,6 +2805,7 @@ export class CodeEmitter {
|
|
|
2587
2805
|
let fixedSource = this.addJsExtensionAndAssertions(source);
|
|
2588
2806
|
if (named[0] === '*' && named.length === 2) return `import ${def}, * as ${named[1]} from ${fixedSource}`;
|
|
2589
2807
|
let names = named.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
|
|
2808
|
+
this._attachImportSpecifierAnchors(sexpr, [def, ...named.flatMap(i => Array.isArray(i) ? i : [i])]);
|
|
2590
2809
|
return `import ${def}, { ${names} } from ${fixedSource}`;
|
|
2591
2810
|
}
|
|
2592
2811
|
let [specifier, source] = rest;
|
|
@@ -2595,11 +2814,70 @@ export class CodeEmitter {
|
|
|
2595
2814
|
if (Array.isArray(specifier)) {
|
|
2596
2815
|
if (specifier[0] === '*' && specifier.length === 2) return `import * as ${specifier[1]} from ${fixedSource}`;
|
|
2597
2816
|
let names = specifier.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
|
|
2817
|
+
this._attachImportSpecifierAnchors(sexpr, specifier.flatMap(i => Array.isArray(i) ? i : [i]));
|
|
2598
2818
|
return `import { ${names} } from ${fixedSource}`;
|
|
2599
2819
|
}
|
|
2600
2820
|
return `import ${this.emit(specifier, 'value')} from ${fixedSource}`;
|
|
2601
2821
|
}
|
|
2602
2822
|
|
|
2823
|
+
// Attach source-map anchors for each named import specifier so hover /
|
|
2824
|
+
// go-to-def works on individual names in `import { foo, bar } from '...'`.
|
|
2825
|
+
// The parser drops .loc from specifier strings, so we recover positions
|
|
2826
|
+
// by scanning the source forward from the `import` keyword.
|
|
2827
|
+
_attachImportSpecifierAnchors(sexpr, names) {
|
|
2828
|
+
if (!sexpr || !sexpr.loc) return;
|
|
2829
|
+
const source = this.options && this.options.source;
|
|
2830
|
+
if (!source || !names || !names.length) return;
|
|
2831
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
2832
|
+
let row = sexpr.loc.r;
|
|
2833
|
+
let col = sexpr.loc.c;
|
|
2834
|
+
const anchors = [];
|
|
2835
|
+
for (const name of names) {
|
|
2836
|
+
if (typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) continue;
|
|
2837
|
+
const re = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
|
|
2838
|
+
let found = false;
|
|
2839
|
+
while (row < lines.length) {
|
|
2840
|
+
const line = lines[row] || '';
|
|
2841
|
+
// Strip trailing line comment so '# foo' doesn't match
|
|
2842
|
+
const codePart = line.replace(/#.*$/, '');
|
|
2843
|
+
re.lastIndex = 0;
|
|
2844
|
+
const slice = codePart.slice(col);
|
|
2845
|
+
const m = re.exec(slice);
|
|
2846
|
+
if (m) {
|
|
2847
|
+
const c = col + m.index;
|
|
2848
|
+
anchors.push({ name, origLine: row, origCol: c });
|
|
2849
|
+
col = c + name.length;
|
|
2850
|
+
found = true;
|
|
2851
|
+
break;
|
|
2852
|
+
}
|
|
2853
|
+
row++;
|
|
2854
|
+
col = 0;
|
|
2855
|
+
}
|
|
2856
|
+
if (!found) break;
|
|
2857
|
+
}
|
|
2858
|
+
if (anchors.length) sexpr._anchors = (sexpr._anchors || []).concat(anchors);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
// Propagate the void marker from a `name! = fn` LHS onto the function node.
|
|
2862
|
+
// The `!` suffix is recorded as `.bang === true` metadata on the target
|
|
2863
|
+
// identifier; when the value is a function (`->`/`=>`/`def`) the bang means
|
|
2864
|
+
// the function is void (no implicit return). We stamp `isVoid` directly on
|
|
2865
|
+
// the function node so the arrow emitters read it locally — the same way
|
|
2866
|
+
// `emitDef` reads `meta(name, 'bang')` off its own node. Used by assignment
|
|
2867
|
+
// and export declaration paths so `export name! = ->` matches the bare
|
|
2868
|
+
// `name! = ->` semantics. Rejects `!`/`&` sigils on non-function values,
|
|
2869
|
+
// exactly like a plain assignment.
|
|
2870
|
+
applyVoidMarker(target, value, sexpr) {
|
|
2871
|
+
let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
|
|
2872
|
+
if (target instanceof String && meta(target, 'bang') !== undefined && !isFnValue) {
|
|
2873
|
+
let sigil = meta(target, 'bang') === true ? '!' : '&';
|
|
2874
|
+
this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
|
|
2875
|
+
}
|
|
2876
|
+
if (target instanceof String && meta(target, 'bang') === true && isFnValue) {
|
|
2877
|
+
value.isVoid = true;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2603
2881
|
emitExport(head, rest) {
|
|
2604
2882
|
let [decl] = rest;
|
|
2605
2883
|
if (this.options.skipExports) {
|
|
@@ -2612,6 +2890,7 @@ export class CodeEmitter {
|
|
|
2612
2890
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2613
2891
|
}
|
|
2614
2892
|
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2893
|
+
this.applyVoidMarker(decl[1], decl[2], decl);
|
|
2615
2894
|
const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2616
2895
|
this._componentName = prev;
|
|
2617
2896
|
this._componentTypeParams = prevTP;
|
|
@@ -2630,6 +2909,7 @@ export class CodeEmitter {
|
|
|
2630
2909
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2631
2910
|
}
|
|
2632
2911
|
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2912
|
+
this.applyVoidMarker(decl[1], decl[2], decl);
|
|
2633
2913
|
const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2634
2914
|
this._componentName = prev;
|
|
2635
2915
|
this._componentTypeParams = prevTP;
|
|
@@ -2643,10 +2923,14 @@ export class CodeEmitter {
|
|
|
2643
2923
|
emitExportDefault(head, rest) {
|
|
2644
2924
|
let [expr] = rest;
|
|
2645
2925
|
if (this.options.skipExports) {
|
|
2646
|
-
if (this.is(expr, '='))
|
|
2926
|
+
if (this.is(expr, '=')) {
|
|
2927
|
+
this.applyVoidMarker(expr[1], expr[2], expr);
|
|
2928
|
+
return `const ${expr[1]} = ${this.emit(expr[2], 'value')}`;
|
|
2929
|
+
}
|
|
2647
2930
|
return this.emit(expr, 'statement');
|
|
2648
2931
|
}
|
|
2649
2932
|
if (this.is(expr, '=')) {
|
|
2933
|
+
this.applyVoidMarker(expr[1], expr[2], expr);
|
|
2650
2934
|
return `const ${expr[1]} = ${this.emit(expr[2], 'value')};\nexport default ${expr[1]}`;
|
|
2651
2935
|
}
|
|
2652
2936
|
return `export default ${this.emit(expr, 'statement')}`;
|
|
@@ -2776,14 +3060,36 @@ export class CodeEmitter {
|
|
|
2776
3060
|
|
|
2777
3061
|
formatParam(param) {
|
|
2778
3062
|
if (typeof param === 'string') return param;
|
|
2779
|
-
if (param instanceof String)
|
|
2780
|
-
|
|
3063
|
+
if (param instanceof String) {
|
|
3064
|
+
// In `inlineTypes` mode (set by typecheck.compileForCheck), emit the
|
|
3065
|
+
// type annotation inline so shadow TS sees `name: T` for typed params
|
|
3066
|
+
// in every function-like position — top-level arrows, class methods,
|
|
3067
|
+
// object-literal method shorthand, nested functions, etc. The user-
|
|
3068
|
+
// facing `-c` output stays untouched because this flag is off by
|
|
3069
|
+
// default. `.type` carries the raw Rip type string (with `::`),
|
|
3070
|
+
// which converts to TS form by swapping `::` → `:`.
|
|
3071
|
+
if (this.options.inlineTypes && param.type) {
|
|
3072
|
+
return `${param.valueOf()}: ${param.type.replace(/::/g, ':')}`;
|
|
3073
|
+
}
|
|
3074
|
+
return param.valueOf();
|
|
3075
|
+
}
|
|
3076
|
+
if (this.is(param, 'rest')) {
|
|
3077
|
+
// Rest param: `...name`. When the name is a String wrapper carrying
|
|
3078
|
+
// a type, emit `...name: T` so shadow TS sees the rest tuple/array.
|
|
3079
|
+
let restName = param[1];
|
|
3080
|
+
if (this.options.inlineTypes && restName instanceof String && restName.type) {
|
|
3081
|
+
return `...${restName.valueOf()}: ${restName.type.replace(/::/g, ':')}`;
|
|
3082
|
+
}
|
|
3083
|
+
return `...${restName}`;
|
|
3084
|
+
}
|
|
2781
3085
|
if (this.is(param, 'default')) {
|
|
2782
3086
|
// `param[1]` is either a plain identifier string (e.g. `x = 5`) or a
|
|
2783
3087
|
// destructuring pattern AST node (e.g. `{a, b} = {}`). Recurse via
|
|
2784
3088
|
// `formatParam` so patterns emit as `{a, b}` / `[x, y]` instead of
|
|
2785
3089
|
// being coerced to a string via `Array.prototype.toString`, which
|
|
2786
3090
|
// produced the famous `(object,,a,a,,b,b = {})` mis-rendering.
|
|
3091
|
+
// The recursion also picks up any inline type annotation on the
|
|
3092
|
+
// name in `inlineTypes` mode, yielding `name: T = default`.
|
|
2787
3093
|
return `${this.formatParam(param[1])} = ${this.emit(param[2], 'value')}`;
|
|
2788
3094
|
}
|
|
2789
3095
|
if (this.is(param, '.') && param[1] === 'this') return param[2];
|
|
@@ -2823,10 +3129,14 @@ export class CodeEmitter {
|
|
|
2823
3129
|
|
|
2824
3130
|
let paramNames = new Set();
|
|
2825
3131
|
let extractPN = (p) => {
|
|
2826
|
-
|
|
3132
|
+
// Unwrap String wrappers — typed params arrive as `new String('name')`
|
|
3133
|
+
// with `.data.type` metadata. Without unwrapping, `paramNames.has('name')`
|
|
3134
|
+
// misses (Set compares wrappers by identity), causing the param name to
|
|
3135
|
+
// be re-hoisted as a local `let`, producing duplicate-declaration errors.
|
|
3136
|
+
if (typeof p === 'string' || p instanceof String) paramNames.add(str(p));
|
|
2827
3137
|
else if (Array.isArray(p)) {
|
|
2828
|
-
if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string') paramNames.add(p[1]); }
|
|
2829
|
-
else if (p[0] === 'default') { if (typeof p[1] === 'string') paramNames.add(p[1]); }
|
|
3138
|
+
if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
|
|
3139
|
+
else if (p[0] === 'default') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
|
|
2830
3140
|
else if (p[0] === 'array' || p[0] === 'object') this.collectVarsFromArray(p, paramNames);
|
|
2831
3141
|
}
|
|
2832
3142
|
};
|
|
@@ -2870,7 +3180,20 @@ export class CodeEmitter {
|
|
|
2870
3180
|
|
|
2871
3181
|
this.indentLevel++;
|
|
2872
3182
|
let code = '{\n';
|
|
2873
|
-
if (newVars.size > 0)
|
|
3183
|
+
if (newVars.size > 0) {
|
|
3184
|
+
// In `inlineTypes` mode, propagate `name:: T = value` annotations from
|
|
3185
|
+
// body-level typed assignments onto the hoisted `let`. Without this,
|
|
3186
|
+
// the hoist emits `let y;` (no type), shadow-TS infers from the first
|
|
3187
|
+
// RHS literal (`y = true` → `y: true`), and any later `y = false`
|
|
3188
|
+
// fails TS2322. With this, the hoist emits `let y: boolean;` and the
|
|
3189
|
+
// body's `y = true` / `y = false` both check cleanly.
|
|
3190
|
+
let typedLocals = this.options.inlineTypes ? this.collectTypedLocals(body) : null;
|
|
3191
|
+
let names = Array.from(newVars).sort().map(n => {
|
|
3192
|
+
let t = typedLocals?.get(n);
|
|
3193
|
+
return t ? `${n}: ${t}` : n;
|
|
3194
|
+
});
|
|
3195
|
+
code += this.indent() + `let ${names.join(', ')};\n`;
|
|
3196
|
+
}
|
|
2874
3197
|
|
|
2875
3198
|
let firstIsSuper = autoAssignments.length > 0 && statements.length > 0 &&
|
|
2876
3199
|
Array.isArray(statements[0]) && statements[0][0] === 'super';
|
|
@@ -3707,7 +4030,7 @@ export class CodeEmitter {
|
|
|
3707
4030
|
|
|
3708
4031
|
containsAwait(sexpr) {
|
|
3709
4032
|
if (!sexpr) return false;
|
|
3710
|
-
if (sexpr instanceof String && meta(sexpr, '
|
|
4033
|
+
if (sexpr instanceof String && meta(sexpr, 'bang') === true) return true;
|
|
3711
4034
|
if (typeof sexpr !== 'object') return false;
|
|
3712
4035
|
if (this.is(sexpr, 'await')) return true;
|
|
3713
4036
|
if (this.is(sexpr, 'for-as') && sexpr[3] === true) return true;
|
|
@@ -4166,7 +4489,13 @@ export class Compiler {
|
|
|
4166
4489
|
// in this file at all — the exporting module strips its `export type` to
|
|
4167
4490
|
// nothing, so leaving the specifier in place would cause a runtime
|
|
4168
4491
|
// "module does not provide an export named X" error.
|
|
4169
|
-
|
|
4492
|
+
//
|
|
4493
|
+
// Skip elision in `inlineTypes` mode (set by typecheck.compileForCheck).
|
|
4494
|
+
// The shadow-TS output is fed to the TypeScript language service, not
|
|
4495
|
+
// executed — so keeping the specifiers preserves go-to-definition and
|
|
4496
|
+
// hover for type-only imports like `RetryConfig` that resolve to
|
|
4497
|
+
// `export type` declarations in the target module.
|
|
4498
|
+
if (lexer.typeRefNames?.size > 0 && !this.options.inlineTypes) {
|
|
4170
4499
|
let usedNames = new Set();
|
|
4171
4500
|
let inImport = false;
|
|
4172
4501
|
for (let t of tokens) {
|
|
@@ -4308,7 +4637,7 @@ export class Compiler {
|
|
|
4308
4637
|
// If only terminators remain (type-only source), emit types and return early
|
|
4309
4638
|
if (tokens.every(t => t[0] === 'TERMINATOR')) {
|
|
4310
4639
|
if (typeTokens && _typesEmitter) dts = _typesEmitter(typeTokens, ['program'], source);
|
|
4311
|
-
return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {} };
|
|
4640
|
+
return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {}, gates: [] };
|
|
4312
4641
|
}
|
|
4313
4642
|
|
|
4314
4643
|
// Step 3: Parse — shim adapter wraps token values with metadata
|
|
@@ -4377,11 +4706,18 @@ export class Compiler {
|
|
|
4377
4706
|
skipDataPart: this.options.skipDataPart,
|
|
4378
4707
|
stubComponents: this.options.stubComponents,
|
|
4379
4708
|
reactiveVars: this.options.reactiveVars,
|
|
4709
|
+
// Emit `name: T` inline on typed params so shadow TS in compileForCheck
|
|
4710
|
+
// sees annotations on every function-like position (top-level arrows,
|
|
4711
|
+
// class methods, object-literal method shorthand, nested functions).
|
|
4712
|
+
// Off by default — only set when producing input for the shadow TS pass.
|
|
4713
|
+
inlineTypes: this.options.inlineTypes,
|
|
4380
4714
|
// Schema runtime mode: 'browser' / 'validate' / 'server' / 'migration'.
|
|
4381
4715
|
// Default 'migration' covers the common case (CLI, server, tests) where
|
|
4382
4716
|
// the user might call any schema feature including .toSQL(). The browser
|
|
4383
4717
|
// bundle build script overrides to 'browser' for size reduction.
|
|
4384
4718
|
schemaMode: this.options.schemaMode,
|
|
4719
|
+
// Opt-in: fold derived projection schemas to self-contained literals.
|
|
4720
|
+
foldProjections: this.options.foldProjections,
|
|
4385
4721
|
sourceMap,
|
|
4386
4722
|
});
|
|
4387
4723
|
let code = generator.compile(sexpr);
|
|
@@ -4408,12 +4744,14 @@ export class Compiler {
|
|
|
4408
4744
|
code += `\n//# sourceMappingURL=${this.options.filename}.js.map`;
|
|
4409
4745
|
}
|
|
4410
4746
|
|
|
4411
|
-
// 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.
|
|
4412
4750
|
if (typeTokens && _typesEmitter) {
|
|
4413
|
-
dts = _typesEmitter(typeTokens, sexpr, source);
|
|
4751
|
+
dts = _typesEmitter(typeTokens, sexpr, source, generator._schemaBehavior, generator._schemaAnon);
|
|
4414
4752
|
}
|
|
4415
4753
|
|
|
4416
|
-
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 || [] };
|
|
4417
4755
|
}
|
|
4418
4756
|
|
|
4419
4757
|
compileToJS(source) { return this.compile(source).code; }
|
|
@@ -4480,6 +4818,126 @@ export function emit(sexpr, options = {}) {
|
|
|
4480
4818
|
return new CodeEmitter(options).compile(sexpr);
|
|
4481
4819
|
}
|
|
4482
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
|
+
|
|
4483
4941
|
export function getReactiveRuntime() {
|
|
4484
4942
|
return new CodeEmitter({}).getReactiveRuntime();
|
|
4485
4943
|
}
|