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/components.js
CHANGED
|
@@ -17,7 +17,7 @@ import { HTML_TAGS, SVG_TAGS, TEMPLATE_TAGS } from './generated/dom-tags.js';
|
|
|
17
17
|
const BIND_PREFIX = '__bind_';
|
|
18
18
|
const BIND_SUFFIX = '__';
|
|
19
19
|
|
|
20
|
-
const LIFECYCLE_HOOKS = new Set(['beforeMount', 'mounted', '
|
|
20
|
+
const LIFECYCLE_HOOKS = new Set(['beforeMount', 'mounted', 'beforeUnmount', 'unmounted', 'onError']);
|
|
21
21
|
const BOOLEAN_ATTRS = new Set([
|
|
22
22
|
'disabled', 'hidden', 'readonly', 'required', 'checked', 'selected',
|
|
23
23
|
'autofocus', 'autoplay', 'controls', 'loop', 'muted', 'multiple',
|
|
@@ -79,6 +79,17 @@ function getMemberType(target) {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Extract `?` optionality flag from s-expression target node.
|
|
84
|
+
* Set by the lexer as `.optional` on the prop-name String wrapper when
|
|
85
|
+
* the source wrote `@label?:: T` or similar.
|
|
86
|
+
*/
|
|
87
|
+
function getMemberOptional(target) {
|
|
88
|
+
if (target instanceof String && target.optional) return true;
|
|
89
|
+
if (Array.isArray(target) && target[2] instanceof String && target[2].optional) return true;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
// ============================================================================
|
|
83
94
|
// Prototype Installation
|
|
84
95
|
// ============================================================================
|
|
@@ -575,9 +586,52 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
575
586
|
const _transferMeta = (from, to) => {
|
|
576
587
|
if (!(from instanceof String)) return to;
|
|
577
588
|
const s = new String(to);
|
|
578
|
-
if (from.
|
|
579
|
-
if (from.
|
|
580
|
-
return (s.
|
|
589
|
+
if (from.optional) s.optional = true;
|
|
590
|
+
if (from.bang) s.bang = true;
|
|
591
|
+
return (s.optional || s.bang) ? s : to;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// Inject `// @rip-src:N` markers onto each statement line of a method body
|
|
595
|
+
// emitted into a component stub. The emitter doesn't carry source-map data
|
|
596
|
+
// for stub method bodies, so without explicit markers, typecheck.js falls
|
|
597
|
+
// back to linear gap-fill interpolation which silently misaligns when stub
|
|
598
|
+
// sizes change. Walks the body s-expression to recover statement source
|
|
599
|
+
// lines and pairs them with the rendered body's non-trivial lines.
|
|
600
|
+
proto.addBodyRipSrcMarkers = function(bodyCode, bodySexpr) {
|
|
601
|
+
if (typeof bodyCode !== 'string' || !bodyCode) return bodyCode;
|
|
602
|
+
const stmts = Array.isArray(bodySexpr) && bodySexpr[0] === 'block'
|
|
603
|
+
? bodySexpr.slice(1)
|
|
604
|
+
: (Array.isArray(bodySexpr) ? [bodySexpr] : []);
|
|
605
|
+
if (stmts.length === 0) return bodyCode;
|
|
606
|
+
const getLoc = (s) => {
|
|
607
|
+
if (s == null) return null;
|
|
608
|
+
if (!Array.isArray(s)) return s?.loc?.r ?? null;
|
|
609
|
+
if (s.loc?.r) return s.loc.r;
|
|
610
|
+
if (s[0]?.loc?.r) return s[0].loc.r;
|
|
611
|
+
for (const child of s) {
|
|
612
|
+
if (child?.loc?.r) return child.loc.r;
|
|
613
|
+
if (Array.isArray(child)) {
|
|
614
|
+
const l = getLoc(child);
|
|
615
|
+
if (l != null) return l;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
};
|
|
620
|
+
const srcLines = stmts.map(getLoc);
|
|
621
|
+
if (srcLines.every(l => l == null)) return bodyCode;
|
|
622
|
+
const lines = bodyCode.split('\n');
|
|
623
|
+
let si = 0;
|
|
624
|
+
for (let i = 0; i < lines.length && si < srcLines.length; i++) {
|
|
625
|
+
const trimmed = lines[i].trim();
|
|
626
|
+
if (!trimmed) continue;
|
|
627
|
+
if (trimmed === '{' || trimmed === '}' || trimmed.startsWith('//')) continue;
|
|
628
|
+
if (lines[i].includes('@rip-src:')) { si++; continue; }
|
|
629
|
+
if (srcLines[si] != null) {
|
|
630
|
+
lines[i] = `${lines[i]} // @rip-src:${srcLines[si]}`;
|
|
631
|
+
}
|
|
632
|
+
si++;
|
|
633
|
+
}
|
|
634
|
+
return lines.join('\n');
|
|
581
635
|
};
|
|
582
636
|
|
|
583
637
|
proto.transformComponentMembers = function(sexpr, localScope = new Set()) {
|
|
@@ -707,31 +761,35 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
707
761
|
reactiveMembers.add(varName);
|
|
708
762
|
}
|
|
709
763
|
} else if (op === '.' && stmt[1] === 'this' && getMemberName(stmt)) {
|
|
710
|
-
//
|
|
764
|
+
// Bare prop form: `(. this name)` — no default value.
|
|
765
|
+
// `@name` → required (caller must pass)
|
|
766
|
+
// `@name?` → optional (caller may omit; value will be undefined)
|
|
767
|
+
// `@name?:: T`→ optional, typed
|
|
711
768
|
const varName = (typeof stmt[2] === 'string' || stmt[2] instanceof String) ? stmt[2].valueOf() : null;
|
|
712
769
|
if (varName) {
|
|
713
|
-
|
|
770
|
+
const optional = getMemberOptional(stmt);
|
|
771
|
+
stateVars.push({ name: varName, value: undefined, isPublic: true, type: stmt[2]?.type || null, required: !optional, optional, srcLine: stmt.loc?.r });
|
|
714
772
|
memberNames.add(varName);
|
|
715
773
|
reactiveMembers.add(varName);
|
|
716
774
|
}
|
|
717
775
|
} else if (op === 'state') {
|
|
718
776
|
const varName = getMemberName(stmt[1]);
|
|
719
777
|
if (varName) {
|
|
720
|
-
stateVars.push({ name: varName, value: stmt[2], isPublic: isPublicProp(stmt[1]), type: getMemberType(stmt[1]) });
|
|
778
|
+
stateVars.push({ name: varName, value: stmt[2], isPublic: isPublicProp(stmt[1]), type: getMemberType(stmt[1]), optional: getMemberOptional(stmt[1]), srcLine: stmt.loc?.r });
|
|
721
779
|
memberNames.add(varName);
|
|
722
780
|
reactiveMembers.add(varName);
|
|
723
781
|
}
|
|
724
782
|
} else if (op === 'computed') {
|
|
725
783
|
const varName = getMemberName(stmt[1]);
|
|
726
784
|
if (varName) {
|
|
727
|
-
derivedVars.push({ name: varName, expr: stmt[2], type: getMemberType(stmt[1]) });
|
|
785
|
+
derivedVars.push({ name: varName, expr: stmt[2], type: getMemberType(stmt[1]), srcLine: stmt.loc?.r });
|
|
728
786
|
memberNames.add(varName);
|
|
729
787
|
reactiveMembers.add(varName);
|
|
730
788
|
}
|
|
731
789
|
} else if (op === 'readonly') {
|
|
732
790
|
const varName = getMemberName(stmt[1]);
|
|
733
791
|
if (varName) {
|
|
734
|
-
readonlyVars.push({ name: varName, value: stmt[2], isPublic: isPublicProp(stmt[1]), type: getMemberType(stmt[1]) });
|
|
792
|
+
readonlyVars.push({ name: varName, value: stmt[2], isPublic: isPublicProp(stmt[1]), type: getMemberType(stmt[1]), optional: getMemberOptional(stmt[1]), srcLine: stmt.loc?.r });
|
|
735
793
|
memberNames.add(varName);
|
|
736
794
|
}
|
|
737
795
|
} else if (op === '=') {
|
|
@@ -745,7 +803,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
745
803
|
methods.push({ name: varName, func: val });
|
|
746
804
|
memberNames.add(varName);
|
|
747
805
|
} else {
|
|
748
|
-
stateVars.push({ name: varName, value: val, isPublic: isPublicProp(stmt[1]) });
|
|
806
|
+
stateVars.push({ name: varName, value: val, isPublic: isPublicProp(stmt[1]), srcLine: stmt.loc?.r });
|
|
749
807
|
memberNames.add(varName);
|
|
750
808
|
reactiveMembers.add(varName);
|
|
751
809
|
}
|
|
@@ -804,28 +862,44 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
804
862
|
|
|
805
863
|
// --- Type-check stub: typed member declarations + body expressions, no DOM ---
|
|
806
864
|
if (this.options.stubComponents) {
|
|
807
|
-
//
|
|
808
|
-
const expandType = (t) => t ? t.replace(/::/g, ':')
|
|
809
|
-
.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined')
|
|
810
|
-
.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined')
|
|
811
|
-
.replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>') : null;
|
|
865
|
+
// Strip Rip's `::` annotation sigil to TypeScript's `:` separator.
|
|
866
|
+
const expandType = (t) => t ? t.replace(/::/g, ':') : null;
|
|
812
867
|
|
|
813
868
|
const sl = [];
|
|
814
869
|
const componentTypeParams = this._componentTypeParams || '';
|
|
815
870
|
sl.push(`class ${componentTypeParams}{`);
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
871
|
+
// Injected `this` shape for every component. The compiler stays
|
|
872
|
+
// context-free; `declare app: any` and `declare router: any` are
|
|
873
|
+
// rewritten by typecheck.js to typed shapes when the project anchor /
|
|
874
|
+
// stash file is discoverable. `params`, `query`, `children`, and the
|
|
875
|
+
// lifecycle hooks are stable across all projects, so they're typed
|
|
876
|
+
// here directly. User-defined hooks are emitted later as real methods;
|
|
877
|
+
// skip the optional-signature declaration for those names to avoid
|
|
878
|
+
// overload-optionality mismatches.
|
|
879
|
+
// Combine all injected declarations onto a single line to preserve the
|
|
880
|
+
// stub's prior line count. Source-map gap-fill interpolates linearly
|
|
881
|
+
// between marker lines (e.g. between @rip-src markers from `@count`
|
|
882
|
+
// and a downstream method), so adding stub header lines pushes the
|
|
883
|
+
// method bodies past where interpolation expects them — breaking
|
|
884
|
+
// @ts-expect-error injection. Keeping a single header line preserves
|
|
885
|
+
// the relative offsets.
|
|
886
|
+
sl.push(' declare _root: Element | null; declare app: any; declare router: any; declare params: Record<string, string>; declare query: URLSearchParams; declare children: any;');
|
|
887
|
+
const userHookNames = new Set(lifecycleHooks.map(h => h.name));
|
|
888
|
+
const hookDecls = [];
|
|
889
|
+
if (!userHookNames.has('beforeMount')) hookDecls.push('beforeMount?(): void;');
|
|
890
|
+
if (!userHookNames.has('mounted')) hookDecls.push('mounted?(): void;');
|
|
891
|
+
if (!userHookNames.has('beforeUnmount')) hookDecls.push('beforeUnmount?(): void;');
|
|
892
|
+
if (!userHookNames.has('unmounted')) hookDecls.push('unmounted?(): void;');
|
|
893
|
+
if (!userHookNames.has('onError')) hookDecls.push('onError?(err: { status?: number; message?: string; error?: Error; path?: string }): void;');
|
|
894
|
+
if (hookDecls.length) sl.push(' ' + hookDecls.join(' '));
|
|
821
895
|
sl.push(' emit(_name: string, _detail?: any): void {}');
|
|
822
896
|
|
|
823
897
|
// Constructor — typed props for public state/readonly (matches DTS)
|
|
824
898
|
const propEntries = [];
|
|
825
|
-
for (const { name, type, isPublic, required } of stateVars) {
|
|
899
|
+
for (const { name, type, isPublic, required, optional } of stateVars) {
|
|
826
900
|
if (!isPublic) continue;
|
|
827
901
|
const ts = expandType(type);
|
|
828
|
-
const opt = required ? '' : '
|
|
902
|
+
const opt = (optional ?? !required) ? '?' : '';
|
|
829
903
|
propEntries.push(`${name}${opt}: ${ts || 'any'}`);
|
|
830
904
|
// Two-way binding: allow parent to pass Signal<T> for this prop
|
|
831
905
|
propEntries.push(`__bind_${name}__?: Signal<${ts || 'any'}>`);
|
|
@@ -836,7 +910,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
836
910
|
propEntries.push(`${name}?: ${ts || 'any'}`);
|
|
837
911
|
}
|
|
838
912
|
{
|
|
839
|
-
const hasRequired = propEntries.length > 0 && stateVars.some(v => v.isPublic && v.required);
|
|
913
|
+
const hasRequired = propEntries.length > 0 && stateVars.some(v => v.isPublic && v.required && !v.optional);
|
|
840
914
|
const propsOpt = hasRequired ? '' : '?';
|
|
841
915
|
let propsType = propEntries.length > 0 ? `{${propEntries.join('; ')}}` : '{}';
|
|
842
916
|
if (inheritsTag) propsType += ` & __RipProps<'${inheritsTag}'>`;
|
|
@@ -854,9 +928,15 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
854
928
|
};
|
|
855
929
|
|
|
856
930
|
// Property declarations (declare avoids definite-assignment errors)
|
|
857
|
-
for (const { name, type, value } of stateVars) {
|
|
931
|
+
for (const { name, type, value, optional, srcLine } of stateVars) {
|
|
858
932
|
const ts = expandType(type) || inferLiteralType(value);
|
|
859
|
-
|
|
933
|
+
// Optional prop with no default: field can be undefined at runtime
|
|
934
|
+
// (we don't synthesize a `?? null` fallback below), so the Signal's
|
|
935
|
+
// payload type must include undefined.
|
|
936
|
+
const optNoDefault = optional && value === undefined;
|
|
937
|
+
const wrapped = ts ? (optNoDefault ? `${ts} | undefined` : ts) : null;
|
|
938
|
+
const marker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
|
|
939
|
+
sl.push((wrapped ? ` declare ${name}: Signal<${wrapped}>;` : ` declare ${name}: Signal<any>;`) + marker);
|
|
860
940
|
}
|
|
861
941
|
if (inheritsTag) {
|
|
862
942
|
sl.push(` declare rest: Signal<__RipProps<'${inheritsTag}'>>;`);
|
|
@@ -880,19 +960,21 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
880
960
|
|
|
881
961
|
// _init body — readonly, state, computed assignments (skip accepted/offered)
|
|
882
962
|
sl.push(' _init(props) {');
|
|
883
|
-
for (const { name, value, isPublic } of readonlyVars) {
|
|
963
|
+
for (const { name, value, isPublic, srcLine } of readonlyVars) {
|
|
884
964
|
const val = this.emitInComponent(value, 'value');
|
|
885
|
-
|
|
965
|
+
const marker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
|
|
966
|
+
sl.push((isPublic ? ` this.${name} = props.${name} ?? ${val};` : ` this.${name} = ${val};`) + marker);
|
|
886
967
|
}
|
|
887
|
-
for (const { name, value, isPublic, required, type } of stateVars) {
|
|
888
|
-
|
|
889
|
-
|
|
968
|
+
for (const { name, value, isPublic, required, type, srcLine } of stateVars) {
|
|
969
|
+
const marker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
|
|
970
|
+
if (isPublic && (required || value === undefined)) {
|
|
971
|
+
sl.push(` this.${name} = __state(props.__bind_${name}__ ?? props.${name});` + marker);
|
|
890
972
|
} else if (isPublic) {
|
|
891
973
|
const val = this.emitInComponent(value, 'value');
|
|
892
|
-
sl.push(` this.${name} = __state(props.__bind_${name}__ ?? props.${name} ?? ${val});`);
|
|
974
|
+
sl.push(` this.${name} = __state(props.__bind_${name}__ ?? props.${name} ?? ${val});` + marker);
|
|
893
975
|
} else {
|
|
894
976
|
const val = this.emitInComponent(value, 'value');
|
|
895
|
-
sl.push(` this.${name} = __state(${val});`);
|
|
977
|
+
sl.push(` this.${name} = __state(${val});` + marker);
|
|
896
978
|
}
|
|
897
979
|
}
|
|
898
980
|
|
|
@@ -976,7 +1058,12 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
976
1058
|
}
|
|
977
1059
|
const transformed = this.reactiveMembers ? this.transformComponentMembers(methodBody) : methodBody;
|
|
978
1060
|
const isAsync = this.containsAwait(methodBody);
|
|
979
|
-
|
|
1061
|
+
let bodyCode = this.emitFunctionBody(transformed, params || []);
|
|
1062
|
+
// Inject @rip-src markers on each body statement line so that
|
|
1063
|
+
// @ts-expect-error injection and hover/diagnostics resolve to the
|
|
1064
|
+
// user's actual source line, instead of relying on linear gap-fill
|
|
1065
|
+
// interpolation across the stub (which is brittle to stub size).
|
|
1066
|
+
bodyCode = this.addBodyRipSrcMarkers(bodyCode, methodBody);
|
|
980
1067
|
sl.push(` ${isAsync ? 'async ' : ''}${name}(${paramStr}) ${bodyCode}`);
|
|
981
1068
|
}
|
|
982
1069
|
}
|
|
@@ -988,7 +1075,8 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
988
1075
|
const paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
|
|
989
1076
|
const transformed = this.reactiveMembers ? this.transformComponentMembers(hookBody) : hookBody;
|
|
990
1077
|
const isAsync = this.containsAwait(hookBody);
|
|
991
|
-
|
|
1078
|
+
let bodyCode = this.emitFunctionBody(transformed, params || []);
|
|
1079
|
+
bodyCode = this.addBodyRipSrcMarkers(bodyCode, hookBody);
|
|
992
1080
|
sl.push(` ${isAsync ? 'async ' : ''}${name}(${paramStr}) ${bodyCode}`);
|
|
993
1081
|
}
|
|
994
1082
|
}
|
|
@@ -1051,8 +1139,22 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1051
1139
|
props.sideExprs = sideExprs;
|
|
1052
1140
|
return props;
|
|
1053
1141
|
};
|
|
1054
|
-
const extractIntrinsicProps = (args) => {
|
|
1142
|
+
const extractIntrinsicProps = (args, tagName) => {
|
|
1055
1143
|
const props = [];
|
|
1144
|
+
// For <a href: ...>, wrap interpolated route literals (templates
|
|
1145
|
+
// starting with `/...`) in __ripRoute so TS checks the dynamic
|
|
1146
|
+
// template against __RipRoutes. Static literals fall through to
|
|
1147
|
+
// the existing __ripEl Option-C conditional, which still gives
|
|
1148
|
+
// the "Did you mean ..." hint. Variables and external URLs are
|
|
1149
|
+
// left untouched.
|
|
1150
|
+
const wrapHrefVal = (val) => {
|
|
1151
|
+
if (tagName !== 'a') return val;
|
|
1152
|
+
if (typeof val !== 'string') return val;
|
|
1153
|
+
if (val.length < 2) return val;
|
|
1154
|
+
if (val[0] !== '`') return val;
|
|
1155
|
+
if (val[1] !== '/') return val;
|
|
1156
|
+
return `__ripRoute(${val})`;
|
|
1157
|
+
};
|
|
1056
1158
|
for (const arg of args) {
|
|
1057
1159
|
let obj = null;
|
|
1058
1160
|
if (this.is(arg, 'object')) {
|
|
@@ -1089,7 +1191,8 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1089
1191
|
props.push({ code: `${propName}: ${val}`, srcLine });
|
|
1090
1192
|
} else {
|
|
1091
1193
|
const val = this.emitInComponent(value, 'value');
|
|
1092
|
-
|
|
1194
|
+
const finalVal = key === 'href' ? wrapHrefVal(val) : val;
|
|
1195
|
+
props.push({ code: `${key}: ${finalVal}`, srcLine });
|
|
1093
1196
|
}
|
|
1094
1197
|
}
|
|
1095
1198
|
}
|
|
@@ -1373,7 +1476,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1373
1476
|
} else if (typeof head === 'string' && !CodeEmitter.GENERATORS[head] && (TEMPLATE_TAGS.has(head.split(/[.#]/)[0]) ||
|
|
1374
1477
|
(/^[a-z][\w-]*$/.test(head) && node.length > 1))) {
|
|
1375
1478
|
const tagName = head.split(/[.#]/)[0];
|
|
1376
|
-
const iProps = extractIntrinsicProps(node.slice(1));
|
|
1479
|
+
const iProps = extractIntrinsicProps(node.slice(1), tagName);
|
|
1377
1480
|
const tagLine = node.loc?.r;
|
|
1378
1481
|
const srcMarker = tagLine != null ? ` // @rip-src:${tagLine}` : '';
|
|
1379
1482
|
if (iProps.length === 0) {
|
|
@@ -1441,7 +1544,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1441
1544
|
|
|
1442
1545
|
// State variables (__state handles signal passthrough)
|
|
1443
1546
|
for (const { name, value, isPublic, required } of stateVars) {
|
|
1444
|
-
if (isPublic && required) {
|
|
1547
|
+
if (isPublic && (required || value === undefined)) {
|
|
1445
1548
|
lines.push(` this.${name} = __state(props.__bind_${name}__ ?? props.${name});`);
|
|
1446
1549
|
} else if (isPublic) {
|
|
1447
1550
|
const val = this.emitInComponent(value, 'value');
|
|
@@ -2727,7 +2830,13 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2727
2830
|
const outerParams = this._loopVarStack.map(v => `${v.itemVar}, ${v.indexVar}`).join(', ');
|
|
2728
2831
|
const outerExtra = outerParams ? `, ${outerParams}` : '';
|
|
2729
2832
|
|
|
2730
|
-
|
|
2833
|
+
// If the iterated collection is reactive, treat direct member access
|
|
2834
|
+
// on the iter var (`item.qty`, `item[0]`, `item.a.b`) as reactive
|
|
2835
|
+
// inside the loop body. hasReactiveDeps consults this flag, so the
|
|
2836
|
+
// existing emit paths wrap those reads in __effect and the per-row
|
|
2837
|
+
// patch function gets populated.
|
|
2838
|
+
const reactiveSource = this.hasReactiveDeps(collection);
|
|
2839
|
+
this._loopVarStack.push({ itemVar, indexVar, reactiveSource });
|
|
2731
2840
|
const itemNode = this.emitTemplateBlock(body);
|
|
2732
2841
|
this._loopVarStack.pop();
|
|
2733
2842
|
const itemCreateLines = this._createLines;
|
|
@@ -3037,6 +3146,16 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
3037
3146
|
return true;
|
|
3038
3147
|
}
|
|
3039
3148
|
|
|
3149
|
+
// Property chain rooted at a `for`-loop iter var whose source is
|
|
3150
|
+
// reactive (e.g. `for item in cart.items` → `item.qty`, `item[0]`,
|
|
3151
|
+
// `item.foo.bar`). Same rationale as the this-rooted case: the source
|
|
3152
|
+
// proxy is reactive at runtime, so any member read may track. Limited
|
|
3153
|
+
// to direct member access on the iter var — aliases and destructuring
|
|
3154
|
+
// are explicitly out of scope.
|
|
3155
|
+
if ((sexpr[0] === '.' || sexpr[0] === '[]') && this._rootsAtReactiveLoopVar(sexpr)) {
|
|
3156
|
+
return true;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3040
3159
|
// Method call on component: [['.', 'this', method], ...args]
|
|
3041
3160
|
// Methods may read reactive state internally — treat as reactive so the
|
|
3042
3161
|
// call gets wrapped in __effect and re-runs when dependencies change.
|
|
@@ -3118,6 +3237,26 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
3118
3237
|
return this._rootsAtThis(sexpr[1]);
|
|
3119
3238
|
};
|
|
3120
3239
|
|
|
3240
|
+
// _rootsAtReactiveLoopVar — true when a `.` / `[]` access chain bottoms
|
|
3241
|
+
// out at a string identifier that matches the itemVar of some frame on
|
|
3242
|
+
// _loopVarStack whose `reactiveSource` is true. See emitTemplateLoop.
|
|
3243
|
+
proto._rootsAtReactiveLoopVar = function(sexpr) {
|
|
3244
|
+
if (typeof sexpr === 'string') {
|
|
3245
|
+
if (!this._loopVarStack || this._loopVarStack.length === 0) return false;
|
|
3246
|
+
// Iterate innermost-first so a shadowed name resolves to its nearest
|
|
3247
|
+
// binding: `for item in reactive` containing `for item in [1,2,3]`
|
|
3248
|
+
// must treat inner reads of `item` as non-reactive.
|
|
3249
|
+
for (let i = this._loopVarStack.length - 1; i >= 0; i--) {
|
|
3250
|
+
const v = this._loopVarStack[i];
|
|
3251
|
+
if (v.itemVar === sexpr) return !!v.reactiveSource;
|
|
3252
|
+
}
|
|
3253
|
+
return false;
|
|
3254
|
+
}
|
|
3255
|
+
if (!Array.isArray(sexpr)) return false;
|
|
3256
|
+
if (sexpr[0] === '.' || sexpr[0] === '[]') return this._rootsAtReactiveLoopVar(sexpr[1]);
|
|
3257
|
+
return false;
|
|
3258
|
+
};
|
|
3259
|
+
|
|
3121
3260
|
// ==========================================================================
|
|
3122
3261
|
// Component Runtime
|
|
3123
3262
|
// ==========================================================================
|