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/dts.js
CHANGED
|
@@ -36,11 +36,11 @@ export const INTRINSIC_TYPE_DECLS = [
|
|
|
36
36
|
"type __RipAttrKeys<T> = { [K in keyof T]-?: K extends 'style' | 'classList' | 'className' | 'nodeValue' | 'textContent' | 'innerHTML' | 'innerText' | 'outerHTML' | 'outerText' | 'scrollLeft' | 'scrollTop' ? never : K extends `on${string}` | `aria${string}Element` | `aria${string}Elements` ? never : T[K] extends (...args: any[]) => any ? never : (<V>() => V extends Pick<T, K> ? 1 : 2) extends (<V>() => V extends { -readonly [P in K]: T[P] } ? 1 : 2) ? K : never }[keyof T] & string;",
|
|
37
37
|
"type __RipEvents<K extends __RipTag> = { [E in keyof HTMLElementEventMap as `@${E}`]?: ((event: RipEvent<HTMLElementEventMap[E], __RipElementMap[K]>) => void) | null };",
|
|
38
38
|
'type RipEvent<E extends Event, T extends EventTarget> = E & { readonly target: T; readonly currentTarget: T };',
|
|
39
|
-
'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean> | __RipClassValue[];',
|
|
39
|
+
'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean | null | undefined> | __RipClassValue[];',
|
|
40
40
|
"type __RipProps<K extends __RipTag> = { [P in __RipAttrKeys<__RipElementMap[K]>]?: __RipElementMap[K][P] } & __RipEvents<K> & { ref?: string; class?: __RipClassValue | __RipClassValue[]; style?: string; [k: `data-${string}`]: any; [k: `aria-${string}`]: any };",
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
-
export const INTRINSIC_FN_DECL = 'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;';
|
|
43
|
+
export const INTRINSIC_FN_DECL = 'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;\ndeclare function __ripRoute<const T extends string>(s: T): T;';
|
|
44
44
|
|
|
45
45
|
export const ARIA_TYPE_DECLS = [
|
|
46
46
|
'type __RipAriaNavHandlers = { next?: () => void; prev?: () => void; first?: () => void; last?: () => void; select?: () => void; dismiss?: () => void; tab?: () => void; char?: () => void; };',
|
|
@@ -67,6 +67,17 @@ export const SIGNAL_FN = 'declare function __state<T>(value: T | Signal<T>): Sig
|
|
|
67
67
|
export const COMPUTED_INTERFACE = 'interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }';
|
|
68
68
|
export const COMPUTED_FN = 'declare function __computed<T>(fn: () => T): Computed<T>;';
|
|
69
69
|
export const EFFECT_FN = 'declare function __effect(fn: () => void | (() => void)): () => void;';
|
|
70
|
+
export const BATCH_FN = 'declare function __batch<T>(fn: () => T): T;';
|
|
71
|
+
|
|
72
|
+
// Names destructured from `globalThis.__rip` in the source. The DTS
|
|
73
|
+
// preamble and the post-compile `declare function` injection both need
|
|
74
|
+
// to skip auto-declaring these names — the explicit binding shadows the
|
|
75
|
+
// global and would otherwise trip TS2630.
|
|
76
|
+
export function ripDestructuredNames(source) {
|
|
77
|
+
if (typeof source !== 'string') return new Set();
|
|
78
|
+
const inside = (source.match(/\{\s*([^}]*?)\s*\}\s*=\s*globalThis\.__rip\b/) || [])[1] || '';
|
|
79
|
+
return new Set(inside.split(',').map(s => s.trim().split(/[:\s]/)[0]).filter(Boolean));
|
|
80
|
+
}
|
|
70
81
|
|
|
71
82
|
// ============================================================================
|
|
72
83
|
// emitTypes — generate .d.ts from annotated tokens + s-expression tree
|
|
@@ -82,7 +93,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
82
93
|
let classFields = new Set(); // Track emitted field names to avoid duplicates
|
|
83
94
|
let usesSignal = false;
|
|
84
95
|
let usesComputed = false;
|
|
96
|
+
let usesBatch = false;
|
|
85
97
|
let usesRipIntrinsicProps = false;
|
|
98
|
+
const explicitlyBound = ripDestructuredNames(source);
|
|
86
99
|
const sourceLines = typeof source === 'string' ? source.split('\n') : [];
|
|
87
100
|
|
|
88
101
|
// Pre-scan: detect reactive operators regardless of type annotations.
|
|
@@ -92,6 +105,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
92
105
|
const tag = tokens[i][0];
|
|
93
106
|
if (tag === 'REACTIVE_ASSIGN') usesSignal = true;
|
|
94
107
|
else if (tag === 'COMPUTED_ASSIGN') usesComputed = true;
|
|
108
|
+
else if (tag === 'IDENTIFIER' && tokens[i][1] === '__batch') usesBatch = true;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
// Format { prop; prop } into multi-line block. Only applies when the
|
|
@@ -195,7 +209,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
195
209
|
let type = tokens[j].data?.type;
|
|
196
210
|
if (type) hasAnyType = true;
|
|
197
211
|
let hasDefault = tokens[j + 1]?.[0] === '=';
|
|
198
|
-
props.push({ kind: 'rename', propName, localName, type: type ?
|
|
212
|
+
props.push({ kind: 'rename', propName, localName, type: type ? tsType(type) : null, hasDefault });
|
|
199
213
|
j++;
|
|
200
214
|
if (hasDefault) j = skipDefault(tokens, j);
|
|
201
215
|
}
|
|
@@ -208,7 +222,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
208
222
|
let type = tokens[j].data?.type;
|
|
209
223
|
if (type) hasAnyType = true;
|
|
210
224
|
let hasDefault = tokens[j + 1]?.[0] === '=';
|
|
211
|
-
props.push({ kind: 'simple', propName: name, type: type ?
|
|
225
|
+
props.push({ kind: 'simple', propName: name, type: type ? tsType(type) : null, hasDefault });
|
|
212
226
|
j++;
|
|
213
227
|
if (hasDefault) j = skipDefault(tokens, j);
|
|
214
228
|
continue;
|
|
@@ -259,7 +273,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
259
273
|
let name = tokens[j][1];
|
|
260
274
|
let type = tokens[j].data?.type;
|
|
261
275
|
names.push(name);
|
|
262
|
-
elemTypes.push(type ?
|
|
276
|
+
elemTypes.push(type ? tsType(type) : null);
|
|
263
277
|
if (type) hasAnyType = true;
|
|
264
278
|
}
|
|
265
279
|
j++;
|
|
@@ -308,8 +322,8 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
308
322
|
let name = tokens[j][1];
|
|
309
323
|
let type = tokens[j].data?.type;
|
|
310
324
|
let paramName = subclassConstructor ? `_${name}` : name;
|
|
311
|
-
params.push(type ? `${paramName}: ${
|
|
312
|
-
if (type) fields.push({ name, type:
|
|
325
|
+
params.push(type ? `${paramName}: ${tsType(type)}` : paramName);
|
|
326
|
+
if (type) fields.push({ name, type: tsType(type) });
|
|
313
327
|
j++;
|
|
314
328
|
}
|
|
315
329
|
continue;
|
|
@@ -321,7 +335,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
321
335
|
if (tokens[j]?.[0] === 'IDENTIFIER') {
|
|
322
336
|
let name = tokens[j][1];
|
|
323
337
|
let type = tokens[j].data?.type;
|
|
324
|
-
params.push(type ? `...${name}: ${
|
|
338
|
+
params.push(type ? `...${name}: ${tsType(type)}` : `...${name}: any[]`);
|
|
325
339
|
j++;
|
|
326
340
|
}
|
|
327
341
|
continue;
|
|
@@ -364,9 +378,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
364
378
|
hasDefault = true;
|
|
365
379
|
}
|
|
366
380
|
|
|
367
|
-
let isOptional = hasDefault || tok.data?.
|
|
381
|
+
let isOptional = hasDefault || tok.data?.optional;
|
|
368
382
|
if (paramType) {
|
|
369
|
-
params.push(`${paramName}${isOptional ? '?' : ''}: ${
|
|
383
|
+
params.push(`${paramName}${isOptional ? '?' : ''}: ${tsType(paramType)}`);
|
|
370
384
|
} else {
|
|
371
385
|
params.push(paramName);
|
|
372
386
|
}
|
|
@@ -486,7 +500,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
486
500
|
let nameToken = ot[1]; // DEF is [0], name is [1]
|
|
487
501
|
let { params: paramList } = collectParams(ot, 2);
|
|
488
502
|
let returnType = nameToken.data?.returnType;
|
|
489
|
-
let ret = returnType ? `: ${
|
|
503
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
490
504
|
let declare = inClass ? '' : (exp ? '' : 'declare ');
|
|
491
505
|
let typeParams = data.typeParams || '';
|
|
492
506
|
if (inClass) {
|
|
@@ -498,7 +512,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
498
512
|
let ext = data.extends ? ` extends ${data.extends}` : '';
|
|
499
513
|
emitBlock(`${exp}interface ${data.name}${params}${ext} `, data.typeText || '{}', '');
|
|
500
514
|
} else {
|
|
501
|
-
let typeText =
|
|
515
|
+
let typeText = tsType(data.typeText || '');
|
|
502
516
|
emitBlock(`${exp}type ${data.name}${params} = `, typeText, ';');
|
|
503
517
|
}
|
|
504
518
|
continue;
|
|
@@ -594,7 +608,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
594
608
|
if (!nameToken) continue;
|
|
595
609
|
let fnName = nameToken[1];
|
|
596
610
|
let returnType = nameToken.data?.returnType;
|
|
597
|
-
if (!returnType && nameToken.data?.
|
|
611
|
+
if (!returnType && nameToken.data?.bang === true) returnType = 'void';
|
|
598
612
|
let typeParams = nameToken.data?.typeParams || '';
|
|
599
613
|
|
|
600
614
|
let { params, endIdx } = collectParams(tokens, i + 2);
|
|
@@ -603,7 +617,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
603
617
|
if (returnType || params.some(p => p.includes(':'))) {
|
|
604
618
|
let exp = exported ? 'export ' : '';
|
|
605
619
|
let declare = inClass ? '' : (exported ? '' : 'declare ');
|
|
606
|
-
let ret = returnType ? `: ${
|
|
620
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
607
621
|
let paramStr = params.join(', ');
|
|
608
622
|
if (inClass) {
|
|
609
623
|
lines.push(`${indent()}${fnName}${typeParams}(${paramStr})${ret};`);
|
|
@@ -664,7 +678,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
664
678
|
}
|
|
665
679
|
|
|
666
680
|
if (returnType || params.some(p => p.includes(':'))) {
|
|
667
|
-
let ret = returnType ? `: ${
|
|
681
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
668
682
|
let paramStr = params.join(', ');
|
|
669
683
|
// Emit field declarations for constructor @param:: type shorthand
|
|
670
684
|
if (methodName === 'constructor' && fields.length) {
|
|
@@ -717,7 +731,12 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
717
731
|
//
|
|
718
732
|
// The function-form is wrong inside bodies because it would create
|
|
719
733
|
// a phantom global that shadows / collides with the actual local.
|
|
720
|
-
|
|
734
|
+
//
|
|
735
|
+
// If the IDENTIFIER carries an explicit `name:: T = arrow` annotation,
|
|
736
|
+
// skip this path and let the typed-variable-assignment path below
|
|
737
|
+
// emit the typed form (otherwise we'd lose the user's annotation in
|
|
738
|
+
// favor of inferred `any` rest-param emission).
|
|
739
|
+
if (tag === 'IDENTIFIER' && !inClass && !t.data?.type &&
|
|
721
740
|
tokens[i + 1]?.[0] === '=' &&
|
|
722
741
|
(tokens[i + 2]?.[0] === 'PARAM_START' || tokens[i + 2]?.[0] === '(')) {
|
|
723
742
|
let fnName = t[1];
|
|
@@ -741,7 +760,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
741
760
|
let paramStr = params.join(', ');
|
|
742
761
|
if (bodyDepth === 0) {
|
|
743
762
|
let declare = exported ? '' : 'declare ';
|
|
744
|
-
let ret = returnType ? `: ${
|
|
763
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
745
764
|
lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
|
|
746
765
|
} else {
|
|
747
766
|
// `any` rather than `unknown` for the inferred-return case.
|
|
@@ -751,7 +770,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
751
770
|
// user didn't provide one. `unknown` would force callers to
|
|
752
771
|
// narrow before using the result, creating new false
|
|
753
772
|
// positives at every call site.
|
|
754
|
-
let ret = returnType ?
|
|
773
|
+
let ret = returnType ? tsType(returnType) : 'any';
|
|
755
774
|
lines.push(`${indent()}let ${fnName}: (${paramStr}) => ${ret};`);
|
|
756
775
|
}
|
|
757
776
|
continue;
|
|
@@ -761,7 +780,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
761
780
|
// Variable assignments with type annotations
|
|
762
781
|
if (tag === 'IDENTIFIER' && t.data?.type) {
|
|
763
782
|
let varName = t[1];
|
|
764
|
-
let type =
|
|
783
|
+
let type = tsType(t.data.type);
|
|
765
784
|
let next = tokens[i + 1];
|
|
766
785
|
|
|
767
786
|
if (next) {
|
|
@@ -795,7 +814,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
795
814
|
if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>') &&
|
|
796
815
|
arrowToken.data?.returnType) {
|
|
797
816
|
// Typed arrow function assignment
|
|
798
|
-
let returnType =
|
|
817
|
+
let returnType = tsType(arrowToken.data.returnType);
|
|
799
818
|
let { params } = collectParams(tokens, i + 2);
|
|
800
819
|
let paramStr = params.join(', ');
|
|
801
820
|
lines.push(`${indent()}${exp}${declare}function ${varName}(${paramStr}): ${returnType};`);
|
|
@@ -860,15 +879,18 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
860
879
|
}
|
|
861
880
|
if (usesSignal) {
|
|
862
881
|
preamble.push(SIGNAL_INTERFACE);
|
|
863
|
-
preamble.push(SIGNAL_FN);
|
|
882
|
+
if (!explicitlyBound.has('__state')) preamble.push(SIGNAL_FN);
|
|
864
883
|
}
|
|
865
884
|
if (usesComputed) {
|
|
866
885
|
preamble.push(COMPUTED_INTERFACE);
|
|
867
|
-
preamble.push(COMPUTED_FN);
|
|
886
|
+
if (!explicitlyBound.has('__computed')) preamble.push(COMPUTED_FN);
|
|
868
887
|
}
|
|
869
|
-
if (usesSignal || usesComputed) {
|
|
888
|
+
if ((usesSignal || usesComputed) && !explicitlyBound.has('__effect')) {
|
|
870
889
|
preamble.push(EFFECT_FN);
|
|
871
890
|
}
|
|
891
|
+
if ((usesSignal || usesComputed || usesBatch) && !explicitlyBound.has('__batch')) {
|
|
892
|
+
preamble.push(BATCH_FN);
|
|
893
|
+
}
|
|
872
894
|
if (hasSchemaDecls) {
|
|
873
895
|
preamble.push(...SCHEMA_INTRINSIC_DECLS);
|
|
874
896
|
}
|
|
@@ -880,25 +902,16 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
880
902
|
}
|
|
881
903
|
|
|
882
904
|
// ============================================================================
|
|
883
|
-
//
|
|
905
|
+
// Convert a Rip type expression to its TypeScript form.
|
|
884
906
|
// ============================================================================
|
|
907
|
+
//
|
|
908
|
+
// Today this only strips the `::` annotation sigil to `:`. Kept as a
|
|
909
|
+
// dedicated function so every call site routes through one place if the
|
|
910
|
+
// conversion ever needs to grow.
|
|
885
911
|
|
|
886
|
-
function
|
|
912
|
+
function tsType(typeStr) {
|
|
887
913
|
if (!typeStr) return typeStr;
|
|
888
|
-
|
|
889
|
-
// Convert :: to : (annotation sigil to type separator)
|
|
890
|
-
typeStr = typeStr.replace(/::/g, ':');
|
|
891
|
-
|
|
892
|
-
// T?? → T | null | undefined
|
|
893
|
-
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined');
|
|
894
|
-
|
|
895
|
-
// T? → T | undefined (but not ?. or ?: which are different)
|
|
896
|
-
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined');
|
|
897
|
-
|
|
898
|
-
// T! → NonNullable<T>
|
|
899
|
-
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>');
|
|
900
|
-
|
|
901
|
-
return typeStr;
|
|
914
|
+
return typeStr.replace(/::/g, ':');
|
|
902
915
|
}
|
|
903
916
|
|
|
904
917
|
// ============================================================================
|
|
@@ -992,18 +1005,18 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
992
1005
|
if (!Array.isArray(member)) continue;
|
|
993
1006
|
let mHead = member[0]?.valueOf?.() ?? member[0];
|
|
994
1007
|
|
|
995
|
-
let target, propName, isProp, type,
|
|
1008
|
+
let target, propName, isProp, type, optional;
|
|
996
1009
|
|
|
997
1010
|
if (mHead === 'state' || mHead === 'readonly' || mHead === 'computed') {
|
|
998
1011
|
target = member[1];
|
|
999
1012
|
isProp = Array.isArray(target) && (target[0]?.valueOf?.() ?? target[0]) === '.' && (target[1]?.valueOf?.() ?? target[1]) === 'this';
|
|
1000
1013
|
propName = isProp ? (target[2]?.valueOf?.() ?? target[2]) : (target?.valueOf?.() ?? target);
|
|
1001
1014
|
type = isProp ? target[2]?.type : target?.type;
|
|
1002
|
-
|
|
1015
|
+
optional = isProp ? !!target[2]?.optional : !!target?.optional;
|
|
1003
1016
|
if (!isProp) {
|
|
1004
1017
|
componentVars.add(propName);
|
|
1005
1018
|
let wrapper = (mHead === 'computed') ? 'Computed' : 'Signal';
|
|
1006
|
-
let typeStr = type ?
|
|
1019
|
+
let typeStr = type ? tsType(type) : (inferLiteralType(member[2]) || 'any');
|
|
1007
1020
|
bodyMembers.push(` ${propName}: ${wrapper}<${typeStr}>;`);
|
|
1008
1021
|
continue;
|
|
1009
1022
|
}
|
|
@@ -1011,7 +1024,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
1011
1024
|
isProp = (member[1]?.valueOf?.() ?? member[1]) === 'this';
|
|
1012
1025
|
propName = isProp ? (member[2]?.valueOf?.() ?? member[2]) : null;
|
|
1013
1026
|
type = isProp ? member[2]?.type : null;
|
|
1014
|
-
|
|
1027
|
+
optional = isProp ? !!member[2]?.optional : false;
|
|
1015
1028
|
if (!isProp && propName) componentVars.add(propName);
|
|
1016
1029
|
} else if (mHead === 'object') {
|
|
1017
1030
|
// Method definitions: (object (: methodName (-> (params...) (block ...))))
|
|
@@ -1041,7 +1054,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
1041
1054
|
for (let p of params) {
|
|
1042
1055
|
let { inner, hasDefault } = unwrapDefault(p);
|
|
1043
1056
|
let pName = inner?.valueOf?.() ?? inner;
|
|
1044
|
-
let pType = inner?.type ?
|
|
1057
|
+
let pType = inner?.type ? tsType(inner.type) : 'any';
|
|
1045
1058
|
// Defaulted params are optional in the type signature so callers
|
|
1046
1059
|
// may omit them.
|
|
1047
1060
|
let opt = hasDefault ? '?' : '';
|
|
@@ -1060,9 +1073,11 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
1060
1073
|
|
|
1061
1074
|
if (!isProp || !propName) continue;
|
|
1062
1075
|
|
|
1063
|
-
let typeStr = type ?
|
|
1064
|
-
|
|
1065
|
-
|
|
1076
|
+
let typeStr = type ? tsType(type) : 'any';
|
|
1077
|
+
// `?` on the prop name is the sole optionality marker;
|
|
1078
|
+
// `:=` defaults do not imply optionality.
|
|
1079
|
+
let opt = optional ? '?' : '';
|
|
1080
|
+
if (!optional) hasRequired = true;
|
|
1066
1081
|
publicProps.push(` ${propName}${opt}: ${typeStr};`);
|
|
1067
1082
|
if (mHead === 'state') {
|
|
1068
1083
|
publicProps.push(` __bind_${propName}__?: Signal<${typeStr}>;`);
|
package/src/lexer.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Design principles:
|
|
9
9
|
// - Every token carries .pre (whitespace count before it)
|
|
10
|
-
// - Every token carries .data (metadata:
|
|
10
|
+
// - Every token carries .data (metadata: bang, optional, quote, etc.)
|
|
11
11
|
// - Every token carries .loc (location: row, col, len)
|
|
12
12
|
// - Indentation is derived from .pre, not tracked during lexing
|
|
13
13
|
// - Token categories use Sets for O(1) membership tests
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
// token.newLine — true if preceded by a newline
|
|
24
24
|
//
|
|
25
25
|
// Identifier suffixes:
|
|
26
|
-
// ! —
|
|
27
|
-
// ? —
|
|
26
|
+
// ! — bang: fetch!() → await fetch() (call site) | foo! = -> → void (definition)
|
|
27
|
+
// ? — optional: empty? → (empty != null) (existence check / optional marker)
|
|
28
28
|
//
|
|
29
29
|
// The 9 tokenizer methods (in priority order):
|
|
30
30
|
// 1. identifier — variables, keywords, properties, ! and ? suffixes
|
|
@@ -246,7 +246,7 @@ let UNARY_MATH = new Set(['!', '~']);
|
|
|
246
246
|
// Regex Patterns
|
|
247
247
|
// ==========================================================================
|
|
248
248
|
|
|
249
|
-
// Identifier: word chars + optional trailing ! (
|
|
249
|
+
// Identifier: word chars + optional trailing ! (bang) or ? (optional)
|
|
250
250
|
// The ? suffix is only captured when NOT followed by . ? ! [ ( to avoid
|
|
251
251
|
// conflict with ?. (optional chaining), ?? (nullish), ?! (presence), ?.( and ?.[
|
|
252
252
|
let IDENTIFIER_RE = /^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+(?:!|[?](?![.?![(]))?)([^\n\S]*:(?![=:]))?/;
|
|
@@ -519,8 +519,10 @@ export class Lexer {
|
|
|
519
519
|
// Handles: variables, keywords, properties, aliases
|
|
520
520
|
//
|
|
521
521
|
// Suffix operators on identifiers:
|
|
522
|
-
// ! →
|
|
523
|
-
//
|
|
522
|
+
// ! → bang: a neutral "trailing !" flag. Resolved by context downstream —
|
|
523
|
+
// dammit/await at a call site (fetch!() → await fetch()), or the void
|
|
524
|
+
// marker at a function definition (foo! = -> / def foo! → no return).
|
|
525
|
+
// ? → optional (existence): empty? → (empty != null)
|
|
524
526
|
//
|
|
525
527
|
// The ? suffix is captured by IDENTIFIER_RE only when NOT followed by
|
|
526
528
|
// . ? [ ( — so x?.y (optional chaining) and x?? (nullish coalescing)
|
|
@@ -655,16 +657,20 @@ export class Lexer {
|
|
|
655
657
|
}
|
|
656
658
|
}
|
|
657
659
|
|
|
658
|
-
// ---
|
|
660
|
+
// --- Bang: trailing ! → context-resolved (await at call site / void at def) ---
|
|
659
661
|
if (id.length > 1 && id.endsWith('!')) {
|
|
660
|
-
data.
|
|
662
|
+
data.bang = true;
|
|
661
663
|
id = id.slice(0, -1);
|
|
662
664
|
}
|
|
663
665
|
|
|
664
|
-
// ---
|
|
665
|
-
//
|
|
666
|
+
// --- Optional marker: trailing ? ---
|
|
667
|
+
// Identifier-position: existence check (`empty?` → `(empty != null)`).
|
|
668
|
+
// Property-name position: optional prop / type field (`@label?:: T`,
|
|
669
|
+
// `{ x?:: T }`, `def f(x?:: T)`). The flag is consumed by the type/
|
|
670
|
+
// component/schema emitters; the runtime semantics for `name?` as an
|
|
671
|
+
// existence check are unchanged.
|
|
666
672
|
if (id.length > 1 && id.endsWith('?')) {
|
|
667
|
-
data.
|
|
673
|
+
data.optional = true;
|
|
668
674
|
id = id.slice(0, -1);
|
|
669
675
|
}
|
|
670
676
|
|
|
@@ -1519,9 +1525,42 @@ export class Lexer {
|
|
|
1519
1525
|
|
|
1520
1526
|
// Walk back to tag parameters for arrow functions
|
|
1521
1527
|
tagParameters() {
|
|
1522
|
-
|
|
1528
|
+
let closeIdx = this.tokens.length - 1;
|
|
1529
|
+
if (this.tokens[closeIdx]?.[0] !== ')') {
|
|
1530
|
+
// Maybe a return-type annotation sits between `)` and the arrow:
|
|
1531
|
+
// `(x:: T):: R ->`. Scan backward over the type expression (balanced
|
|
1532
|
+
// brackets) looking for the trailing `TYPE_ANNOTATION` whose previous
|
|
1533
|
+
// token is `)`. If found, treat that `)` as the param-list close.
|
|
1534
|
+
let n = this.tokens.length;
|
|
1535
|
+
let depth = 0;
|
|
1536
|
+
let j = n - 1;
|
|
1537
|
+
let found = -1;
|
|
1538
|
+
while (j >= 0) {
|
|
1539
|
+
let tk = this.tokens[j];
|
|
1540
|
+
let tg = tk[0];
|
|
1541
|
+
if (tg === ')' || tg === ']' || tg === '}' || tg === 'CALL_END' ||
|
|
1542
|
+
tg === 'PARAM_END' || tg === 'INDEX_END' ||
|
|
1543
|
+
(tg === 'COMPARE' && tk[1] === '>')) {
|
|
1544
|
+
depth++;
|
|
1545
|
+
} else if (tg === '(' || tg === '[' || tg === '{' || tg === 'CALL_START' ||
|
|
1546
|
+
tg === 'PARAM_START' || tg === 'INDEX_START' ||
|
|
1547
|
+
(tg === 'COMPARE' && tk[1] === '<')) {
|
|
1548
|
+
depth--;
|
|
1549
|
+
} else if (depth === 0) {
|
|
1550
|
+
if (tg === 'TYPE_ANNOTATION') {
|
|
1551
|
+
if (j > 0 && this.tokens[j - 1][0] === ')') found = j - 1;
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
if (tg === 'TERMINATOR' || tg === 'INDENT' || tg === 'OUTDENT' ||
|
|
1555
|
+
tg === '=' || tg === '->' || tg === '=>') break;
|
|
1556
|
+
}
|
|
1557
|
+
j--;
|
|
1558
|
+
}
|
|
1559
|
+
if (found < 0) return this.tagDoIife();
|
|
1560
|
+
closeIdx = found;
|
|
1561
|
+
}
|
|
1523
1562
|
|
|
1524
|
-
let i =
|
|
1563
|
+
let i = closeIdx;
|
|
1525
1564
|
let stack = [];
|
|
1526
1565
|
this.tokens[i][0] = 'PARAM_END';
|
|
1527
1566
|
|
|
@@ -1536,7 +1575,7 @@ export class Lexer {
|
|
|
1536
1575
|
tok[0] = 'PARAM_START';
|
|
1537
1576
|
return this.tagDoIife(i - 1);
|
|
1538
1577
|
} else {
|
|
1539
|
-
this.tokens[
|
|
1578
|
+
this.tokens[closeIdx][0] = 'CALL_END';
|
|
1540
1579
|
return;
|
|
1541
1580
|
}
|
|
1542
1581
|
}
|
|
@@ -1562,10 +1601,14 @@ export class Lexer {
|
|
|
1562
1601
|
this.closeMergeAssignments();
|
|
1563
1602
|
this.closeOpenCalls();
|
|
1564
1603
|
this.closeOpenIndexes();
|
|
1604
|
+
// rewriteTypes must run BEFORE normalizeLines: otherwise a type-arrow
|
|
1605
|
+
// `=>` inside `(...) => T` gets treated as a single-liner function and
|
|
1606
|
+
// wrapped in spurious INDENT/OUTDENT, which then derails the type
|
|
1607
|
+
// collector (it slurps the whole assignment body as part of the type).
|
|
1608
|
+
this.rewriteTypes();
|
|
1565
1609
|
this.normalizeLines();
|
|
1566
1610
|
this.rewriteRender?.();
|
|
1567
1611
|
this.rewriteSchema?.();
|
|
1568
|
-
this.rewriteTypes();
|
|
1569
1612
|
this.tagPostfixConditionals();
|
|
1570
1613
|
this.rewriteTaggedTemplates();
|
|
1571
1614
|
this.addImplicitBracesAndParens();
|
package/src/schema/schema.js
CHANGED
|
@@ -541,7 +541,7 @@ function parseFieldedLine(kind, line, entries, ctx) {
|
|
|
541
541
|
dname === 'one' || dname === 'many' || dname === 'mixin') {
|
|
542
542
|
let t0 = argTokens[0];
|
|
543
543
|
if (t0 && (t0[0] === 'IDENTIFIER' || t0[0] === 'PROPERTY')) {
|
|
544
|
-
let optional = t0.data?.
|
|
544
|
+
let optional = t0.data?.optional === true;
|
|
545
545
|
if (!optional && argTokens[1]?.[0] === '?') optional = true;
|
|
546
546
|
args = [{ target: t0[1], optional }];
|
|
547
547
|
}
|
|
@@ -1468,9 +1468,9 @@ function compileDirectiveArgsLiteral(name, tokens) {
|
|
|
1468
1468
|
}
|
|
1469
1469
|
let target = t0[1];
|
|
1470
1470
|
// `@belongs_to User?` tokenizes as IDENTIFIER "User" with
|
|
1471
|
-
// data.
|
|
1471
|
+
// data.optional=true. A trailing `?` in a later token position is
|
|
1472
1472
|
// also accepted for robustness.
|
|
1473
|
-
let optional = t0.data?.
|
|
1473
|
+
let optional = t0.data?.optional === true;
|
|
1474
1474
|
let pos = 1;
|
|
1475
1475
|
if (!optional && tokens[pos]?.[0] === '?') { optional = true; pos++; }
|
|
1476
1476
|
let parts = [`target: ${JSON.stringify(target)}`];
|
|
@@ -1673,8 +1673,8 @@ function parseBodyTokens(bodyTokens) {
|
|
|
1673
1673
|
function collectModifiers(identToken) {
|
|
1674
1674
|
let mods = [];
|
|
1675
1675
|
let d = identToken.data;
|
|
1676
|
-
if (d?.
|
|
1677
|
-
if (d?.
|
|
1676
|
+
if (d?.bang === true) mods.push('!');
|
|
1677
|
+
if (d?.optional === true) mods.push('?');
|
|
1678
1678
|
return mods;
|
|
1679
1679
|
}
|
|
1680
1680
|
|