rip-lang 3.16.0 → 3.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/rip +162 -10
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +109 -17
- package/docs/RIP-LANG.md +4 -5
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +933 -338
- package/docs/dist/rip.min.js +209 -204
- 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/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/ui/bundle.json +55 -55
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/index.html +1 -1
- package/package.json +9 -4
- package/rip-loader.js +59 -2
- package/src/AGENTS.md +5 -5
- package/src/browser.js +52 -11
- package/src/compiler.js +318 -44
- package/src/components.js +178 -39
- package/src/dts.js +62 -47
- package/src/lexer.js +58 -15
- package/src/schema/schema.js +5 -5
- package/src/typecheck.js +1355 -100
- package/src/types.js +85 -5
- /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
|
@@ -322,6 +322,20 @@ export class CodeEmitter {
|
|
|
322
322
|
// Each entry pairs a statement's generated code with its source loc.
|
|
323
323
|
// Output line positions are computed by exact arithmetic — no heuristics.
|
|
324
324
|
buildMappings() {
|
|
325
|
+
// Imports are emitted at the top of the file (before the preamble),
|
|
326
|
+
// starting at line 0. Process them first with their own line offset.
|
|
327
|
+
if (this._importEntries) {
|
|
328
|
+
let importLineOffset = 0;
|
|
329
|
+
for (let entry of this._importEntries) {
|
|
330
|
+
if (entry.loc) {
|
|
331
|
+
this.sourceMap.addMapping(importLineOffset, 0, entry.loc.r, entry.loc.c);
|
|
332
|
+
}
|
|
333
|
+
if (entry.sexpr && entry.loc) {
|
|
334
|
+
this.recordSubMappings(entry.code, entry.sexpr, importLineOffset);
|
|
335
|
+
}
|
|
336
|
+
importLineOffset += entry.code.split('\n').length;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
325
339
|
if (!this._stmtEntries) return;
|
|
326
340
|
let lineOffset = this._preambleLines;
|
|
327
341
|
for (let entry of this._stmtEntries) {
|
|
@@ -423,6 +437,47 @@ export class CodeEmitter {
|
|
|
423
437
|
ripSrcCache.set(genLineInStmt, v);
|
|
424
438
|
return v;
|
|
425
439
|
};
|
|
440
|
+
// Inline type annotations (emitted when `inlineTypes: true`) inject
|
|
441
|
+
// identifiers into function-signature lines that have no source
|
|
442
|
+
// counterpart — e.g. `header(name, value, opts: { append?: boolean })`.
|
|
443
|
+
// Their identifiers (`append`, `boolean`, etc.) would otherwise compete
|
|
444
|
+
// with real body identifiers in the regex matcher below, mis-mapping
|
|
445
|
+
// source positions onto the type literal. Detect those brace ranges
|
|
446
|
+
// per line and skip matches inside them.
|
|
447
|
+
const inlineTypeRangesCache = new Map();
|
|
448
|
+
const getInlineTypeRanges = (genLineInStmt) => {
|
|
449
|
+
if (inlineTypeRangesCache.has(genLineInStmt)) return inlineTypeRangesCache.get(genLineInStmt);
|
|
450
|
+
const lt = codeLines[genLineInStmt];
|
|
451
|
+
const ranges = [];
|
|
452
|
+
// Only look at function-signature shaped lines: contain `(...) {` or `(...) =>`.
|
|
453
|
+
if (lt && /\)\s*(\{|=>)\s*$/.test(lt)) {
|
|
454
|
+
// Find `: {` after an identifier (with optional `?` and whitespace).
|
|
455
|
+
const annotRe = /\b[a-zA-Z_$][\w$]*\??\s*:\s*\{/g;
|
|
456
|
+
let am;
|
|
457
|
+
while ((am = annotRe.exec(lt)) !== null) {
|
|
458
|
+
const braceStart = am.index + am[0].length - 1; // position of `{`
|
|
459
|
+
// Skip inside strings (defensive — unlikely here)
|
|
460
|
+
if (CodeEmitter._isColInsideString(lt, braceStart)) continue;
|
|
461
|
+
// Walk to matching `}` honoring brace depth and strings
|
|
462
|
+
let depth = 1, j = braceStart + 1, inStr = false, quote = '';
|
|
463
|
+
while (j < lt.length && depth > 0) {
|
|
464
|
+
const ch = lt[j];
|
|
465
|
+
if (inStr) {
|
|
466
|
+
if (ch === '\\') { j += 2; continue; }
|
|
467
|
+
if (ch === quote) inStr = false;
|
|
468
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
469
|
+
inStr = true; quote = ch;
|
|
470
|
+
} else if (ch === '{') depth++;
|
|
471
|
+
else if (ch === '}') depth--;
|
|
472
|
+
j++;
|
|
473
|
+
}
|
|
474
|
+
ranges.push([braceStart, j]); // exclude positions [braceStart, j)
|
|
475
|
+
annotRe.lastIndex = j;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
inlineTypeRangesCache.set(genLineInStmt, ranges);
|
|
479
|
+
return ranges;
|
|
480
|
+
};
|
|
426
481
|
for (let { name, origLine, origCol } of subs) {
|
|
427
482
|
let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
428
483
|
let re = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
@@ -442,6 +497,11 @@ export class CodeEmitter {
|
|
|
442
497
|
const annotSrc = getRipSrcAnnot(genLineInStmt);
|
|
443
498
|
const annotMatches = annotSrc != null && annotSrc === origLine;
|
|
444
499
|
if (lineText && CodeEmitter._isColInsideString(lineText, genCol) && !annotMatches) continue;
|
|
500
|
+
// Skip matches inside inline type annotation brace ranges (e.g.
|
|
501
|
+
// `opts: { append?: boolean }`) — those identifiers are emitted
|
|
502
|
+
// for TS type-checking only and have no real source counterpart.
|
|
503
|
+
const itRanges = getInlineTypeRanges(genLineInStmt);
|
|
504
|
+
if (itRanges.length && itRanges.some(([s, e]) => genCol >= s && genCol < e)) continue;
|
|
445
505
|
let genLine = lineOffset + genLineInStmt;
|
|
446
506
|
// Annotation-matched lines are the authoritative gen position for
|
|
447
507
|
// their source line — score them as a perfect line match so they
|
|
@@ -501,14 +561,37 @@ export class CodeEmitter {
|
|
|
501
561
|
}
|
|
502
562
|
}
|
|
503
563
|
// 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]))
|
|
564
|
+
else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]|^\.{1,3}$|^def$|^class$|^state$|^computed$|^readonly$|^for-/.test(head)) {
|
|
565
|
+
if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1])) {
|
|
566
|
+
ident = node[1];
|
|
567
|
+
// Spread `...x`: node.loc.c marks the `...` start; shift past the
|
|
568
|
+
// operator so the anchor lands on the operand identifier.
|
|
569
|
+
if (head === '...') identCol = node.loc.c + 3;
|
|
570
|
+
}
|
|
506
571
|
}
|
|
507
572
|
// Function call (head is identifier)
|
|
508
573
|
else if (typeof head === 'string' && /^[a-zA-Z_$]/.test(head)) {
|
|
509
574
|
ident = head;
|
|
510
575
|
}
|
|
511
576
|
if (ident) result.push({ name: ident, origLine: node.loc.r, origCol: identCol });
|
|
577
|
+
|
|
578
|
+
// Arrow body bare-identifier anchor: a single-expression arrow body
|
|
579
|
+
// like `-> products` parses as `['->', [], ['block', 'products']]`.
|
|
580
|
+
// The body atom has no .loc (parser only attaches loc to arrays), and
|
|
581
|
+
// the `block` wrapper has bogus `loc=0:0`, so the identifier reference
|
|
582
|
+
// is invisible to the heuristic mapping. Synthesize an anchor by
|
|
583
|
+
// scanning source forward from the arrow's location.
|
|
584
|
+
if ((head === '->' || head === '=>') && Array.isArray(node[2]) && str(node[2][0]) === 'block') {
|
|
585
|
+
const body = node[2];
|
|
586
|
+
for (let i = 1; i < body.length; i++) {
|
|
587
|
+
const leaf = body[i];
|
|
588
|
+
const leafStr = typeof leaf === 'string' || leaf instanceof String ? str(leaf) : null;
|
|
589
|
+
if (leafStr && /^[a-zA-Z_$][\w$]*$/.test(leafStr) && !leaf.loc) {
|
|
590
|
+
const anchor = this._scanForIdentAfter(leafStr, node.loc);
|
|
591
|
+
if (anchor) result.push(anchor);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
512
595
|
}
|
|
513
596
|
// Side-channel anchors attached by walkRender for bare-identifier
|
|
514
597
|
// children of template-tag nodes (e.g. `error` in `p.error error`).
|
|
@@ -528,6 +611,30 @@ export class CodeEmitter {
|
|
|
528
611
|
}
|
|
529
612
|
}
|
|
530
613
|
|
|
614
|
+
// Scan original source for the first occurrence of `ident` after the
|
|
615
|
+
// given start location (typically an arrow's `->` loc). Skips matches
|
|
616
|
+
// inside string/comment regions. Returns a sub-expression anchor or null.
|
|
617
|
+
_scanForIdentAfter(ident, startLoc) {
|
|
618
|
+
const source = this.options && this.options.source;
|
|
619
|
+
if (!source || !startLoc) return null;
|
|
620
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
621
|
+
const re = new RegExp('\\b' + ident.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
|
|
622
|
+
const startRow = startLoc.r;
|
|
623
|
+
const startCol = startLoc.c;
|
|
624
|
+
// Search current line from startCol forward, then up to 20 subsequent lines.
|
|
625
|
+
for (let r = startRow; r < Math.min(lines.length, startRow + 20); r++) {
|
|
626
|
+
const line = lines[r];
|
|
627
|
+
if (!line) continue;
|
|
628
|
+
re.lastIndex = r === startRow ? startCol : 0;
|
|
629
|
+
let m;
|
|
630
|
+
while ((m = re.exec(line)) !== null) {
|
|
631
|
+
if (CodeEmitter._isColInsideString(line, m.index)) continue;
|
|
632
|
+
return { name: ident, origLine: r, origCol: m.index };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
531
638
|
// ---------------------------------------------------------------------------
|
|
532
639
|
// Variable collection
|
|
533
640
|
// ---------------------------------------------------------------------------
|
|
@@ -577,7 +684,12 @@ export class CodeEmitter {
|
|
|
577
684
|
}
|
|
578
685
|
|
|
579
686
|
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
580
|
-
|
|
687
|
+
// Don't hoist loop vars: emitForIn/emitForOf/emitForAs already
|
|
688
|
+
// emit `for (let x of ...)`, which is block-scoped AND gives JS's
|
|
689
|
+
// per-iteration binding semantics (critical for closures captured
|
|
690
|
+
// inside the loop). Hoisting `let x` to the surrounding scope
|
|
691
|
+
// would shadow that as dead code (TS6133 "declared but never
|
|
692
|
+
// read") without changing runtime behavior.
|
|
581
693
|
rest.slice(1).forEach(item => this.collectProgramVariables(item));
|
|
582
694
|
return;
|
|
583
695
|
}
|
|
@@ -640,7 +752,10 @@ export class CodeEmitter {
|
|
|
640
752
|
return;
|
|
641
753
|
}
|
|
642
754
|
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
643
|
-
|
|
755
|
+
// See collectProgramVariables for-loop branch: loop vars are
|
|
756
|
+
// block-scoped via the for-header's `let`; hoisting them here
|
|
757
|
+
// would only produce a redundant outer `let x` flagged as
|
|
758
|
+
// unused.
|
|
644
759
|
rest.slice(1).forEach(collect);
|
|
645
760
|
return;
|
|
646
761
|
}
|
|
@@ -668,20 +783,55 @@ export class CodeEmitter {
|
|
|
668
783
|
return vars;
|
|
669
784
|
}
|
|
670
785
|
|
|
786
|
+
// Walk a function body and collect typed local assignments. Returns a
|
|
787
|
+
// Map<name, typeString> for every `name:: T = value` whose target is a
|
|
788
|
+
// String-wrapped identifier carrying `data.type` (attached by types.js).
|
|
789
|
+
//
|
|
790
|
+
// Used by `emitBodyWithReturns` in `inlineTypes` mode to annotate the
|
|
791
|
+
// function-top hoist (`let a, y: boolean, b;`) so shadow-TS sees the
|
|
792
|
+
// intended type instead of inferring a literal from the first RHS. Stops
|
|
793
|
+
// at nested function boundaries — each function owns its own typed locals.
|
|
794
|
+
//
|
|
795
|
+
// Conflict policy: first annotation wins. Same-name re-annotations are
|
|
796
|
+
// silently ignored; mixing different types on the same local is an
|
|
797
|
+
// unusual pattern best surfaced by TS itself once the hoist carries the
|
|
798
|
+
// first annotation.
|
|
799
|
+
collectTypedLocals(body) {
|
|
800
|
+
let typed = new Map();
|
|
801
|
+
let walk = (sexpr) => {
|
|
802
|
+
if (!Array.isArray(sexpr)) return;
|
|
803
|
+
let [head, ...rest] = sexpr;
|
|
804
|
+
head = str(head);
|
|
805
|
+
if (Array.isArray(head)) { sexpr.forEach(walk); return; }
|
|
806
|
+
if (CodeEmitter.ASSIGNMENT_OPS.has(head)) {
|
|
807
|
+
let [target, value] = rest;
|
|
808
|
+
if (target instanceof String && target.type && !typed.has(str(target))) {
|
|
809
|
+
typed.set(str(target), target.type);
|
|
810
|
+
}
|
|
811
|
+
walk(value);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (head === 'def' || head === '->' || head === '=>' || head === 'effect') return;
|
|
815
|
+
rest.forEach(walk);
|
|
816
|
+
};
|
|
817
|
+
walk(body);
|
|
818
|
+
return typed;
|
|
819
|
+
}
|
|
820
|
+
|
|
671
821
|
// ---------------------------------------------------------------------------
|
|
672
822
|
// Main dispatch
|
|
673
823
|
// ---------------------------------------------------------------------------
|
|
674
824
|
|
|
675
825
|
emit(sexpr, context = 'statement') {
|
|
676
|
-
// String object with metadata (quote,
|
|
826
|
+
// String object with metadata (quote, bang, optional, heregex, etc.)
|
|
677
827
|
if (sexpr instanceof String) {
|
|
678
828
|
// Dammit operator (!)
|
|
679
|
-
if (meta(sexpr, '
|
|
829
|
+
if (meta(sexpr, 'bang') === true) {
|
|
680
830
|
return `await ${str(sexpr)}()`;
|
|
681
831
|
}
|
|
682
832
|
|
|
683
833
|
// Existence check (?)
|
|
684
|
-
if (meta(sexpr, '
|
|
834
|
+
if (meta(sexpr, 'optional')) {
|
|
685
835
|
return `(${str(sexpr)} != null)`;
|
|
686
836
|
}
|
|
687
837
|
|
|
@@ -737,8 +887,8 @@ export class CodeEmitter {
|
|
|
737
887
|
|
|
738
888
|
let [head, ...rest] = sexpr;
|
|
739
889
|
|
|
740
|
-
// Preserve
|
|
741
|
-
let
|
|
890
|
+
// Preserve bang metadata before converting head to primitive
|
|
891
|
+
let headBangMeta = meta(head, 'bang');
|
|
742
892
|
head = str(head);
|
|
743
893
|
|
|
744
894
|
// Dispatch table
|
|
@@ -758,7 +908,7 @@ export class CodeEmitter {
|
|
|
758
908
|
let postfix = this._tryPostfixCall(head, rest, context);
|
|
759
909
|
if (postfix) return postfix;
|
|
760
910
|
|
|
761
|
-
let needsAwait =
|
|
911
|
+
let needsAwait = headBangMeta === true;
|
|
762
912
|
let callStr = `${this.emit(head, 'value')}(${this._emitArgs(rest)})`;
|
|
763
913
|
return needsAwait ? `await ${callStr}` : callStr;
|
|
764
914
|
}
|
|
@@ -784,15 +934,16 @@ export class CodeEmitter {
|
|
|
784
934
|
let postfix = this._tryPostfixCall(head, rest, context);
|
|
785
935
|
if (postfix) return postfix;
|
|
786
936
|
|
|
787
|
-
// Property access with
|
|
937
|
+
// Property access with bang sigil on property
|
|
788
938
|
let needsAwait = false;
|
|
789
939
|
let calleeCode;
|
|
790
|
-
if (head[0] === '.' && meta(head[2], '
|
|
940
|
+
if (head[0] === '.' && meta(head[2], 'bang') === true) {
|
|
791
941
|
needsAwait = true;
|
|
792
942
|
let [obj, prop] = head.slice(1);
|
|
793
943
|
let objCode = this.emit(obj, 'value');
|
|
794
944
|
let needsParens = CodeEmitter.NUMBER_LITERAL_RE.test(objCode) ||
|
|
795
|
-
((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield')))
|
|
945
|
+
((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield'))) ||
|
|
946
|
+
/^(await|yield)\s/.test(objCode);
|
|
796
947
|
let base = needsParens ? `(${objCode})` : objCode;
|
|
797
948
|
calleeCode = `${base}.${str(prop)}`;
|
|
798
949
|
} else {
|
|
@@ -850,7 +1001,12 @@ export class CodeEmitter {
|
|
|
850
1001
|
let needsBlank = false;
|
|
851
1002
|
|
|
852
1003
|
if (imports.length > 0) {
|
|
853
|
-
|
|
1004
|
+
let importEntries = imports.map(s => {
|
|
1005
|
+
let generated = this.addSemicolon(s, this.emit(s, 'statement'));
|
|
1006
|
+
return { code: generated, loc: Array.isArray(s) ? s.loc : null, sexpr: Array.isArray(s) ? s : null };
|
|
1007
|
+
});
|
|
1008
|
+
this._importEntries = importEntries;
|
|
1009
|
+
code += importEntries.map(e => e.code).join('\n');
|
|
854
1010
|
needsBlank = true;
|
|
855
1011
|
}
|
|
856
1012
|
|
|
@@ -1042,15 +1198,7 @@ export class CodeEmitter {
|
|
|
1042
1198
|
}
|
|
1043
1199
|
|
|
1044
1200
|
// 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
|
-
}
|
|
1201
|
+
this.applyVoidMarker(target, value, sexpr);
|
|
1054
1202
|
|
|
1055
1203
|
// Empty destructuring — just evaluate RHS
|
|
1056
1204
|
let isEmptyArr = this.is(target, 'array', 0);
|
|
@@ -1139,7 +1287,7 @@ export class CodeEmitter {
|
|
|
1139
1287
|
|
|
1140
1288
|
// Generate target (handle reactive, sigils)
|
|
1141
1289
|
let targetCode;
|
|
1142
|
-
if (target instanceof String && meta(target, '
|
|
1290
|
+
if (target instanceof String && meta(target, 'bang') !== undefined) {
|
|
1143
1291
|
targetCode = str(target);
|
|
1144
1292
|
} else if (typeof target === 'string' && this.reactiveVars?.has(target)) {
|
|
1145
1293
|
targetCode = `${target}.value`;
|
|
@@ -1186,8 +1334,8 @@ export class CodeEmitter {
|
|
|
1186
1334
|
objCode.startsWith('await ') ||
|
|
1187
1335
|
((this.is(obj, 'object') || this.is(obj, 'yield')));
|
|
1188
1336
|
let base = needsParens ? `(${objCode})` : objCode;
|
|
1189
|
-
if (meta(prop, '
|
|
1190
|
-
if (meta(prop, '
|
|
1337
|
+
if (meta(prop, 'bang') === true) return `await ${base}.${str(prop)}()`;
|
|
1338
|
+
if (meta(prop, 'optional')) return `(${base}.${str(prop)} != null)`;
|
|
1191
1339
|
return `${base}.${str(prop)}`;
|
|
1192
1340
|
}
|
|
1193
1341
|
|
|
@@ -1353,7 +1501,7 @@ export class CodeEmitter {
|
|
|
1353
1501
|
|
|
1354
1502
|
emitDef(head, rest, context, sexpr) {
|
|
1355
1503
|
let [name, params, body] = rest;
|
|
1356
|
-
let sideEffectOnly = meta(name, '
|
|
1504
|
+
let sideEffectOnly = meta(name, 'bang') === true;
|
|
1357
1505
|
let cleanName = str(name);
|
|
1358
1506
|
let paramList = this.emitParamList(params);
|
|
1359
1507
|
let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
|
|
@@ -1365,8 +1513,7 @@ export class CodeEmitter {
|
|
|
1365
1513
|
emitThinArrow(head, rest, context, sexpr) {
|
|
1366
1514
|
let [params, body] = rest;
|
|
1367
1515
|
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
|
|
1368
|
-
let sideEffectOnly =
|
|
1369
|
-
this.nextFunctionIsVoid = false;
|
|
1516
|
+
let sideEffectOnly = sexpr.isVoid || false;
|
|
1370
1517
|
let paramList = this.emitParamList(params);
|
|
1371
1518
|
let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
|
|
1372
1519
|
let isAsync = this.containsAwait(body);
|
|
@@ -1378,8 +1525,7 @@ export class CodeEmitter {
|
|
|
1378
1525
|
emitFatArrow(head, rest, context, sexpr) {
|
|
1379
1526
|
let [params, body] = rest;
|
|
1380
1527
|
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
|
|
1381
|
-
let sideEffectOnly =
|
|
1382
|
-
this.nextFunctionIsVoid = false;
|
|
1528
|
+
let sideEffectOnly = sexpr.isVoid || false;
|
|
1383
1529
|
let paramList = this.emitParamList(params);
|
|
1384
1530
|
let isSingle = params.length === 1 && typeof params[0] === 'string' &&
|
|
1385
1531
|
!paramList.includes('=') && !paramList.includes('...') &&
|
|
@@ -1896,7 +2042,20 @@ export class CodeEmitter {
|
|
|
1896
2042
|
// Symbol literals
|
|
1897
2043
|
// ---------------------------------------------------------------------------
|
|
1898
2044
|
|
|
1899
|
-
emitSymbol(head, rest
|
|
2045
|
+
emitSymbol(head, rest, context, sexpr) {
|
|
2046
|
+
// Anchor the symbol name's source position to the generated `Symbol`
|
|
2047
|
+
// identifier so hover on `:foo` shows `SymbolConstructor` (the
|
|
2048
|
+
// generated `"foo"` is a string literal that TS has no hover for).
|
|
2049
|
+
// sexpr.loc points at the `:`; the name starts at col + 1.
|
|
2050
|
+
if (sexpr && sexpr.loc && typeof rest[0] === 'string') {
|
|
2051
|
+
sexpr._anchors = (sexpr._anchors || []).concat([{
|
|
2052
|
+
name: 'Symbol',
|
|
2053
|
+
origLine: sexpr.loc.r,
|
|
2054
|
+
origCol: sexpr.loc.c + 1,
|
|
2055
|
+
}]);
|
|
2056
|
+
}
|
|
2057
|
+
return `Symbol.for(${JSON.stringify(rest[0])})`;
|
|
2058
|
+
}
|
|
1900
2059
|
|
|
1901
2060
|
// ---------------------------------------------------------------------------
|
|
1902
2061
|
// Data structures
|
|
@@ -1996,8 +2155,7 @@ export class CodeEmitter {
|
|
|
1996
2155
|
if (operator === ':' && isSimpleKey && this.is(value, '->')) {
|
|
1997
2156
|
let [, mParams, mBody] = value;
|
|
1998
2157
|
if ((!mParams || (Array.isArray(mParams) && mParams.length === 0)) && this.containsIt(mBody)) mParams = ['it'];
|
|
1999
|
-
let mSideEffect =
|
|
2000
|
-
this.nextFunctionIsVoid = false;
|
|
2158
|
+
let mSideEffect = value.isVoid || false;
|
|
2001
2159
|
let mParamList = this.emitParamList(mParams);
|
|
2002
2160
|
let mBodyCode = this.emitFunctionBody(mBody, mParams, mSideEffect);
|
|
2003
2161
|
let mIsAsync = this.containsAwait(mBody);
|
|
@@ -2578,7 +2736,7 @@ export class CodeEmitter {
|
|
|
2578
2736
|
emitImport(head, rest, context, sexpr) {
|
|
2579
2737
|
if (rest.length === 1) {
|
|
2580
2738
|
let importExpr = `import(${this.emit(rest[0], 'value')})`;
|
|
2581
|
-
if (meta(sexpr[0], '
|
|
2739
|
+
if (meta(sexpr[0], 'bang') === true) return `(await ${importExpr})`;
|
|
2582
2740
|
return importExpr;
|
|
2583
2741
|
}
|
|
2584
2742
|
if (this.options.skipImports) return '';
|
|
@@ -2587,6 +2745,7 @@ export class CodeEmitter {
|
|
|
2587
2745
|
let fixedSource = this.addJsExtensionAndAssertions(source);
|
|
2588
2746
|
if (named[0] === '*' && named.length === 2) return `import ${def}, * as ${named[1]} from ${fixedSource}`;
|
|
2589
2747
|
let names = named.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
|
|
2748
|
+
this._attachImportSpecifierAnchors(sexpr, [def, ...named.flatMap(i => Array.isArray(i) ? i : [i])]);
|
|
2590
2749
|
return `import ${def}, { ${names} } from ${fixedSource}`;
|
|
2591
2750
|
}
|
|
2592
2751
|
let [specifier, source] = rest;
|
|
@@ -2595,11 +2754,70 @@ export class CodeEmitter {
|
|
|
2595
2754
|
if (Array.isArray(specifier)) {
|
|
2596
2755
|
if (specifier[0] === '*' && specifier.length === 2) return `import * as ${specifier[1]} from ${fixedSource}`;
|
|
2597
2756
|
let names = specifier.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
|
|
2757
|
+
this._attachImportSpecifierAnchors(sexpr, specifier.flatMap(i => Array.isArray(i) ? i : [i]));
|
|
2598
2758
|
return `import { ${names} } from ${fixedSource}`;
|
|
2599
2759
|
}
|
|
2600
2760
|
return `import ${this.emit(specifier, 'value')} from ${fixedSource}`;
|
|
2601
2761
|
}
|
|
2602
2762
|
|
|
2763
|
+
// Attach source-map anchors for each named import specifier so hover /
|
|
2764
|
+
// go-to-def works on individual names in `import { foo, bar } from '...'`.
|
|
2765
|
+
// The parser drops .loc from specifier strings, so we recover positions
|
|
2766
|
+
// by scanning the source forward from the `import` keyword.
|
|
2767
|
+
_attachImportSpecifierAnchors(sexpr, names) {
|
|
2768
|
+
if (!sexpr || !sexpr.loc) return;
|
|
2769
|
+
const source = this.options && this.options.source;
|
|
2770
|
+
if (!source || !names || !names.length) return;
|
|
2771
|
+
const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
|
|
2772
|
+
let row = sexpr.loc.r;
|
|
2773
|
+
let col = sexpr.loc.c;
|
|
2774
|
+
const anchors = [];
|
|
2775
|
+
for (const name of names) {
|
|
2776
|
+
if (typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) continue;
|
|
2777
|
+
const re = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
|
|
2778
|
+
let found = false;
|
|
2779
|
+
while (row < lines.length) {
|
|
2780
|
+
const line = lines[row] || '';
|
|
2781
|
+
// Strip trailing line comment so '# foo' doesn't match
|
|
2782
|
+
const codePart = line.replace(/#.*$/, '');
|
|
2783
|
+
re.lastIndex = 0;
|
|
2784
|
+
const slice = codePart.slice(col);
|
|
2785
|
+
const m = re.exec(slice);
|
|
2786
|
+
if (m) {
|
|
2787
|
+
const c = col + m.index;
|
|
2788
|
+
anchors.push({ name, origLine: row, origCol: c });
|
|
2789
|
+
col = c + name.length;
|
|
2790
|
+
found = true;
|
|
2791
|
+
break;
|
|
2792
|
+
}
|
|
2793
|
+
row++;
|
|
2794
|
+
col = 0;
|
|
2795
|
+
}
|
|
2796
|
+
if (!found) break;
|
|
2797
|
+
}
|
|
2798
|
+
if (anchors.length) sexpr._anchors = (sexpr._anchors || []).concat(anchors);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// Propagate the void marker from a `name! = fn` LHS onto the function node.
|
|
2802
|
+
// The `!` suffix is recorded as `.bang === true` metadata on the target
|
|
2803
|
+
// identifier; when the value is a function (`->`/`=>`/`def`) the bang means
|
|
2804
|
+
// the function is void (no implicit return). We stamp `isVoid` directly on
|
|
2805
|
+
// the function node so the arrow emitters read it locally — the same way
|
|
2806
|
+
// `emitDef` reads `meta(name, 'bang')` off its own node. Used by assignment
|
|
2807
|
+
// and export declaration paths so `export name! = ->` matches the bare
|
|
2808
|
+
// `name! = ->` semantics. Rejects `!`/`&` sigils on non-function values,
|
|
2809
|
+
// exactly like a plain assignment.
|
|
2810
|
+
applyVoidMarker(target, value, sexpr) {
|
|
2811
|
+
let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
|
|
2812
|
+
if (target instanceof String && meta(target, 'bang') !== undefined && !isFnValue) {
|
|
2813
|
+
let sigil = meta(target, 'bang') === true ? '!' : '&';
|
|
2814
|
+
this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
|
|
2815
|
+
}
|
|
2816
|
+
if (target instanceof String && meta(target, 'bang') === true && isFnValue) {
|
|
2817
|
+
value.isVoid = true;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2603
2821
|
emitExport(head, rest) {
|
|
2604
2822
|
let [decl] = rest;
|
|
2605
2823
|
if (this.options.skipExports) {
|
|
@@ -2612,6 +2830,7 @@ export class CodeEmitter {
|
|
|
2612
2830
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2613
2831
|
}
|
|
2614
2832
|
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2833
|
+
this.applyVoidMarker(decl[1], decl[2], decl);
|
|
2615
2834
|
const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2616
2835
|
this._componentName = prev;
|
|
2617
2836
|
this._componentTypeParams = prevTP;
|
|
@@ -2630,6 +2849,7 @@ export class CodeEmitter {
|
|
|
2630
2849
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2631
2850
|
}
|
|
2632
2851
|
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2852
|
+
this.applyVoidMarker(decl[1], decl[2], decl);
|
|
2633
2853
|
const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2634
2854
|
this._componentName = prev;
|
|
2635
2855
|
this._componentTypeParams = prevTP;
|
|
@@ -2643,10 +2863,14 @@ export class CodeEmitter {
|
|
|
2643
2863
|
emitExportDefault(head, rest) {
|
|
2644
2864
|
let [expr] = rest;
|
|
2645
2865
|
if (this.options.skipExports) {
|
|
2646
|
-
if (this.is(expr, '='))
|
|
2866
|
+
if (this.is(expr, '=')) {
|
|
2867
|
+
this.applyVoidMarker(expr[1], expr[2], expr);
|
|
2868
|
+
return `const ${expr[1]} = ${this.emit(expr[2], 'value')}`;
|
|
2869
|
+
}
|
|
2647
2870
|
return this.emit(expr, 'statement');
|
|
2648
2871
|
}
|
|
2649
2872
|
if (this.is(expr, '=')) {
|
|
2873
|
+
this.applyVoidMarker(expr[1], expr[2], expr);
|
|
2650
2874
|
return `const ${expr[1]} = ${this.emit(expr[2], 'value')};\nexport default ${expr[1]}`;
|
|
2651
2875
|
}
|
|
2652
2876
|
return `export default ${this.emit(expr, 'statement')}`;
|
|
@@ -2776,14 +3000,36 @@ export class CodeEmitter {
|
|
|
2776
3000
|
|
|
2777
3001
|
formatParam(param) {
|
|
2778
3002
|
if (typeof param === 'string') return param;
|
|
2779
|
-
if (param instanceof String)
|
|
2780
|
-
|
|
3003
|
+
if (param instanceof String) {
|
|
3004
|
+
// In `inlineTypes` mode (set by typecheck.compileForCheck), emit the
|
|
3005
|
+
// type annotation inline so shadow TS sees `name: T` for typed params
|
|
3006
|
+
// in every function-like position — top-level arrows, class methods,
|
|
3007
|
+
// object-literal method shorthand, nested functions, etc. The user-
|
|
3008
|
+
// facing `-c` output stays untouched because this flag is off by
|
|
3009
|
+
// default. `.type` carries the raw Rip type string (with `::`),
|
|
3010
|
+
// which converts to TS form by swapping `::` → `:`.
|
|
3011
|
+
if (this.options.inlineTypes && param.type) {
|
|
3012
|
+
return `${param.valueOf()}: ${param.type.replace(/::/g, ':')}`;
|
|
3013
|
+
}
|
|
3014
|
+
return param.valueOf();
|
|
3015
|
+
}
|
|
3016
|
+
if (this.is(param, 'rest')) {
|
|
3017
|
+
// Rest param: `...name`. When the name is a String wrapper carrying
|
|
3018
|
+
// a type, emit `...name: T` so shadow TS sees the rest tuple/array.
|
|
3019
|
+
let restName = param[1];
|
|
3020
|
+
if (this.options.inlineTypes && restName instanceof String && restName.type) {
|
|
3021
|
+
return `...${restName.valueOf()}: ${restName.type.replace(/::/g, ':')}`;
|
|
3022
|
+
}
|
|
3023
|
+
return `...${restName}`;
|
|
3024
|
+
}
|
|
2781
3025
|
if (this.is(param, 'default')) {
|
|
2782
3026
|
// `param[1]` is either a plain identifier string (e.g. `x = 5`) or a
|
|
2783
3027
|
// destructuring pattern AST node (e.g. `{a, b} = {}`). Recurse via
|
|
2784
3028
|
// `formatParam` so patterns emit as `{a, b}` / `[x, y]` instead of
|
|
2785
3029
|
// being coerced to a string via `Array.prototype.toString`, which
|
|
2786
3030
|
// produced the famous `(object,,a,a,,b,b = {})` mis-rendering.
|
|
3031
|
+
// The recursion also picks up any inline type annotation on the
|
|
3032
|
+
// name in `inlineTypes` mode, yielding `name: T = default`.
|
|
2787
3033
|
return `${this.formatParam(param[1])} = ${this.emit(param[2], 'value')}`;
|
|
2788
3034
|
}
|
|
2789
3035
|
if (this.is(param, '.') && param[1] === 'this') return param[2];
|
|
@@ -2823,10 +3069,14 @@ export class CodeEmitter {
|
|
|
2823
3069
|
|
|
2824
3070
|
let paramNames = new Set();
|
|
2825
3071
|
let extractPN = (p) => {
|
|
2826
|
-
|
|
3072
|
+
// Unwrap String wrappers — typed params arrive as `new String('name')`
|
|
3073
|
+
// with `.data.type` metadata. Without unwrapping, `paramNames.has('name')`
|
|
3074
|
+
// misses (Set compares wrappers by identity), causing the param name to
|
|
3075
|
+
// be re-hoisted as a local `let`, producing duplicate-declaration errors.
|
|
3076
|
+
if (typeof p === 'string' || p instanceof String) paramNames.add(str(p));
|
|
2827
3077
|
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]); }
|
|
3078
|
+
if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
|
|
3079
|
+
else if (p[0] === 'default') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
|
|
2830
3080
|
else if (p[0] === 'array' || p[0] === 'object') this.collectVarsFromArray(p, paramNames);
|
|
2831
3081
|
}
|
|
2832
3082
|
};
|
|
@@ -2870,7 +3120,20 @@ export class CodeEmitter {
|
|
|
2870
3120
|
|
|
2871
3121
|
this.indentLevel++;
|
|
2872
3122
|
let code = '{\n';
|
|
2873
|
-
if (newVars.size > 0)
|
|
3123
|
+
if (newVars.size > 0) {
|
|
3124
|
+
// In `inlineTypes` mode, propagate `name:: T = value` annotations from
|
|
3125
|
+
// body-level typed assignments onto the hoisted `let`. Without this,
|
|
3126
|
+
// the hoist emits `let y;` (no type), shadow-TS infers from the first
|
|
3127
|
+
// RHS literal (`y = true` → `y: true`), and any later `y = false`
|
|
3128
|
+
// fails TS2322. With this, the hoist emits `let y: boolean;` and the
|
|
3129
|
+
// body's `y = true` / `y = false` both check cleanly.
|
|
3130
|
+
let typedLocals = this.options.inlineTypes ? this.collectTypedLocals(body) : null;
|
|
3131
|
+
let names = Array.from(newVars).sort().map(n => {
|
|
3132
|
+
let t = typedLocals?.get(n);
|
|
3133
|
+
return t ? `${n}: ${t}` : n;
|
|
3134
|
+
});
|
|
3135
|
+
code += this.indent() + `let ${names.join(', ')};\n`;
|
|
3136
|
+
}
|
|
2874
3137
|
|
|
2875
3138
|
let firstIsSuper = autoAssignments.length > 0 && statements.length > 0 &&
|
|
2876
3139
|
Array.isArray(statements[0]) && statements[0][0] === 'super';
|
|
@@ -3707,7 +3970,7 @@ export class CodeEmitter {
|
|
|
3707
3970
|
|
|
3708
3971
|
containsAwait(sexpr) {
|
|
3709
3972
|
if (!sexpr) return false;
|
|
3710
|
-
if (sexpr instanceof String && meta(sexpr, '
|
|
3973
|
+
if (sexpr instanceof String && meta(sexpr, 'bang') === true) return true;
|
|
3711
3974
|
if (typeof sexpr !== 'object') return false;
|
|
3712
3975
|
if (this.is(sexpr, 'await')) return true;
|
|
3713
3976
|
if (this.is(sexpr, 'for-as') && sexpr[3] === true) return true;
|
|
@@ -4166,7 +4429,13 @@ export class Compiler {
|
|
|
4166
4429
|
// in this file at all — the exporting module strips its `export type` to
|
|
4167
4430
|
// nothing, so leaving the specifier in place would cause a runtime
|
|
4168
4431
|
// "module does not provide an export named X" error.
|
|
4169
|
-
|
|
4432
|
+
//
|
|
4433
|
+
// Skip elision in `inlineTypes` mode (set by typecheck.compileForCheck).
|
|
4434
|
+
// The shadow-TS output is fed to the TypeScript language service, not
|
|
4435
|
+
// executed — so keeping the specifiers preserves go-to-definition and
|
|
4436
|
+
// hover for type-only imports like `RetryConfig` that resolve to
|
|
4437
|
+
// `export type` declarations in the target module.
|
|
4438
|
+
if (lexer.typeRefNames?.size > 0 && !this.options.inlineTypes) {
|
|
4170
4439
|
let usedNames = new Set();
|
|
4171
4440
|
let inImport = false;
|
|
4172
4441
|
for (let t of tokens) {
|
|
@@ -4377,6 +4646,11 @@ export class Compiler {
|
|
|
4377
4646
|
skipDataPart: this.options.skipDataPart,
|
|
4378
4647
|
stubComponents: this.options.stubComponents,
|
|
4379
4648
|
reactiveVars: this.options.reactiveVars,
|
|
4649
|
+
// Emit `name: T` inline on typed params so shadow TS in compileForCheck
|
|
4650
|
+
// sees annotations on every function-like position (top-level arrows,
|
|
4651
|
+
// class methods, object-literal method shorthand, nested functions).
|
|
4652
|
+
// Off by default — only set when producing input for the shadow TS pass.
|
|
4653
|
+
inlineTypes: this.options.inlineTypes,
|
|
4380
4654
|
// Schema runtime mode: 'browser' / 'validate' / 'server' / 'migration'.
|
|
4381
4655
|
// Default 'migration' covers the common case (CLI, server, tests) where
|
|
4382
4656
|
// the user might call any schema feature including .toSQL(). The browser
|