rip-lang 3.15.4 → 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 +6 -4
- package/bin/rip +167 -12
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +808 -0
- package/docs/RIP-DUCKDB.md +477 -0
- package/docs/RIP-INTRO.md +396 -0
- package/docs/RIP-LANG.md +59 -5
- package/docs/RIP-SCHEMA.md +191 -8
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +3627 -1470
- package/docs/dist/rip.min.js +671 -244
- 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/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/vscode/print/index.html +2 -1
- package/docs/extensions/vscode/print/print-1.0.13.vsix +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 +61 -0
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +0 -7
- package/docs/ui/index.css +66 -23
- package/docs/ui/index.html +6 -6
- package/package.json +9 -3
- package/rip-loader.js +64 -2
- package/src/AGENTS.md +63 -36
- package/src/browser.js +96 -14
- package/src/compiler.js +960 -143
- package/src/components.js +794 -88
- package/src/{types-emit.js → dts.js} +181 -71
- package/src/grammar/README.md +1 -1
- package/src/grammar/grammar.rip +111 -97
- package/src/lexer.js +132 -18
- package/src/parser.js +203 -205
- package/src/repl.js +74 -6
- package/src/schema/runtime-orm.js +168 -4
- package/src/schema/runtime-validate.js +146 -2
- package/src/schema/runtime.generated.js +314 -6
- package/src/schema/schema.js +5 -5
- package/src/sourcemaps.js +277 -1
- package/src/stdlib.js +253 -0
- package/src/typecheck.js +2023 -106
- package/src/types.js +127 -7
- package/docs/ui/accordion.rip +0 -103
- package/docs/ui/alert-dialog.rip +0 -53
- package/docs/ui/autocomplete.rip +0 -115
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -47
- package/docs/ui/button-group.rip +0 -26
- package/docs/ui/button.rip +0 -23
- package/docs/ui/card.rip +0 -25
- package/docs/ui/carousel.rip +0 -110
- package/docs/ui/checkbox-group.rip +0 -61
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -130
- package/docs/ui/context-menu.rip +0 -88
- package/docs/ui/date-picker.rip +0 -206
- package/docs/ui/dialog.rip +0 -60
- package/docs/ui/drawer.rip +0 -58
- package/docs/ui/editable-value.rip +0 -82
- package/docs/ui/field.rip +0 -53
- package/docs/ui/fieldset.rip +0 -22
- package/docs/ui/form.rip +0 -39
- package/docs/ui/grid.rip +0 -901
- package/docs/ui/input-group.rip +0 -28
- package/docs/ui/input.rip +0 -36
- package/docs/ui/label.rip +0 -16
- package/docs/ui/menu.rip +0 -134
- package/docs/ui/menubar.rip +0 -151
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -203
- package/docs/ui/native-select.rip +0 -33
- package/docs/ui/nav-menu.rip +0 -126
- package/docs/ui/number-field.rip +0 -162
- package/docs/ui/otp-field.rip +0 -89
- package/docs/ui/pagination.rip +0 -123
- package/docs/ui/popover.rip +0 -93
- package/docs/ui/preview-card.rip +0 -75
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -57
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -151
- package/docs/ui/separator.rip +0 -17
- package/docs/ui/skeleton.rip +0 -22
- package/docs/ui/slider.rip +0 -165
- package/docs/ui/spinner.rip +0 -17
- package/docs/ui/table.rip +0 -27
- package/docs/ui/tabs.rip +0 -113
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -71
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -38
- package/docs/ui/tooltip.rip +0 -85
- package/src/app.rip +0 -1571
- package/src/sourcemap-merge.js +0 -287
- /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/schema/{dts-emit.js → dts.js} +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
|
}
|
|
@@ -784,6 +842,14 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
784
842
|
for (const { name, isPublic } of stateVars) if (isPublic) publicPropNames.add(name);
|
|
785
843
|
for (const { name, isPublic } of readonlyVars) if (isPublic) publicPropNames.add(name);
|
|
786
844
|
|
|
845
|
+
// When extends-ing a tag, expose `@rest` as a reactive view of caller props
|
|
846
|
+
// not consumed by declared @props. Reads (e.g. `@rest.disabled`) track via
|
|
847
|
+
// `this.rest.value`; `_setRestProp` mutates and calls `.touch()` to notify.
|
|
848
|
+
if (inheritsTag) {
|
|
849
|
+
memberNames.add('rest');
|
|
850
|
+
reactiveMembers.add('rest');
|
|
851
|
+
}
|
|
852
|
+
|
|
787
853
|
// Save and set component context
|
|
788
854
|
const prevComponentMembers = this.componentMembers;
|
|
789
855
|
const prevReactiveMembers = this.reactiveMembers;
|
|
@@ -796,24 +862,44 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
796
862
|
|
|
797
863
|
// --- Type-check stub: typed member declarations + body expressions, no DOM ---
|
|
798
864
|
if (this.options.stubComponents) {
|
|
799
|
-
//
|
|
800
|
-
const expandType = (t) => t ? t.replace(/::/g, ':')
|
|
801
|
-
.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined')
|
|
802
|
-
.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined')
|
|
803
|
-
.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;
|
|
804
867
|
|
|
805
868
|
const sl = [];
|
|
806
869
|
const componentTypeParams = this._componentTypeParams || '';
|
|
807
870
|
sl.push(`class ${componentTypeParams}{`);
|
|
808
|
-
|
|
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(' '));
|
|
809
895
|
sl.push(' emit(_name: string, _detail?: any): void {}');
|
|
810
896
|
|
|
811
897
|
// Constructor — typed props for public state/readonly (matches DTS)
|
|
812
898
|
const propEntries = [];
|
|
813
|
-
for (const { name, type, isPublic, required } of stateVars) {
|
|
899
|
+
for (const { name, type, isPublic, required, optional } of stateVars) {
|
|
814
900
|
if (!isPublic) continue;
|
|
815
901
|
const ts = expandType(type);
|
|
816
|
-
const opt = required ? '' : '
|
|
902
|
+
const opt = (optional ?? !required) ? '?' : '';
|
|
817
903
|
propEntries.push(`${name}${opt}: ${ts || 'any'}`);
|
|
818
904
|
// Two-way binding: allow parent to pass Signal<T> for this prop
|
|
819
905
|
propEntries.push(`__bind_${name}__?: Signal<${ts || 'any'}>`);
|
|
@@ -824,7 +910,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
824
910
|
propEntries.push(`${name}?: ${ts || 'any'}`);
|
|
825
911
|
}
|
|
826
912
|
{
|
|
827
|
-
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);
|
|
828
914
|
const propsOpt = hasRequired ? '' : '?';
|
|
829
915
|
let propsType = propEntries.length > 0 ? `{${propEntries.join('; ')}}` : '{}';
|
|
830
916
|
if (inheritsTag) propsType += ` & __RipProps<'${inheritsTag}'>`;
|
|
@@ -842,9 +928,18 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
842
928
|
};
|
|
843
929
|
|
|
844
930
|
// Property declarations (declare avoids definite-assignment errors)
|
|
845
|
-
for (const { name, type, value } of stateVars) {
|
|
931
|
+
for (const { name, type, value, optional, srcLine } of stateVars) {
|
|
846
932
|
const ts = expandType(type) || inferLiteralType(value);
|
|
847
|
-
|
|
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);
|
|
940
|
+
}
|
|
941
|
+
if (inheritsTag) {
|
|
942
|
+
sl.push(` declare rest: Signal<__RipProps<'${inheritsTag}'>>;`);
|
|
848
943
|
}
|
|
849
944
|
for (const { name, type, value } of readonlyVars) {
|
|
850
945
|
const ts = expandType(type) || inferLiteralType(value);
|
|
@@ -865,19 +960,21 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
865
960
|
|
|
866
961
|
// _init body — readonly, state, computed assignments (skip accepted/offered)
|
|
867
962
|
sl.push(' _init(props) {');
|
|
868
|
-
for (const { name, value, isPublic } of readonlyVars) {
|
|
963
|
+
for (const { name, value, isPublic, srcLine } of readonlyVars) {
|
|
869
964
|
const val = this.emitInComponent(value, 'value');
|
|
870
|
-
|
|
965
|
+
const marker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
|
|
966
|
+
sl.push((isPublic ? ` this.${name} = props.${name} ?? ${val};` : ` this.${name} = ${val};`) + marker);
|
|
871
967
|
}
|
|
872
|
-
for (const { name, value, isPublic, required, type } of stateVars) {
|
|
873
|
-
|
|
874
|
-
|
|
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);
|
|
875
972
|
} else if (isPublic) {
|
|
876
973
|
const val = this.emitInComponent(value, 'value');
|
|
877
|
-
sl.push(` this.${name} = __state(props.__bind_${name}__ ?? props.${name} ?? ${val});`);
|
|
974
|
+
sl.push(` this.${name} = __state(props.__bind_${name}__ ?? props.${name} ?? ${val});` + marker);
|
|
878
975
|
} else {
|
|
879
976
|
const val = this.emitInComponent(value, 'value');
|
|
880
|
-
sl.push(` this.${name} = __state(${val});`);
|
|
977
|
+
sl.push(` this.${name} = __state(${val});` + marker);
|
|
881
978
|
}
|
|
882
979
|
}
|
|
883
980
|
|
|
@@ -886,11 +983,16 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
886
983
|
const isAsync = this.containsAwait(effectBody) ? 'async ' : '';
|
|
887
984
|
if (this.is(effectBody, 'block')) {
|
|
888
985
|
const transformed = this.transformComponentMembers(effectBody);
|
|
889
|
-
|
|
986
|
+
// sideEffectOnly: false so the body's last expression is
|
|
987
|
+
// returned. The reactive runtime treats a returned function as
|
|
988
|
+
// the effect's cleanup — that's how '~> ARIA.bindPopover ...'
|
|
989
|
+
// gets its disposer registered. Disposer is also auto-tracked
|
|
990
|
+
// via __getCurrentComponent for unmount cleanup.
|
|
991
|
+
const body = this.emitFunctionBody(transformed);
|
|
890
992
|
sl.push(` __effect(${isAsync}() => ${body});`);
|
|
891
993
|
} else {
|
|
892
994
|
const effectCode = this.emitInComponent(effectBody, 'value');
|
|
893
|
-
sl.push(` __effect(${isAsync}() => { ${effectCode}; });`);
|
|
995
|
+
sl.push(` __effect(${isAsync}() => { return ${effectCode}; });`);
|
|
894
996
|
}
|
|
895
997
|
}
|
|
896
998
|
sl.push(' }');
|
|
@@ -956,7 +1058,12 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
956
1058
|
}
|
|
957
1059
|
const transformed = this.reactiveMembers ? this.transformComponentMembers(methodBody) : methodBody;
|
|
958
1060
|
const isAsync = this.containsAwait(methodBody);
|
|
959
|
-
|
|
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);
|
|
960
1067
|
sl.push(` ${isAsync ? 'async ' : ''}${name}(${paramStr}) ${bodyCode}`);
|
|
961
1068
|
}
|
|
962
1069
|
}
|
|
@@ -968,7 +1075,8 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
968
1075
|
const paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
|
|
969
1076
|
const transformed = this.reactiveMembers ? this.transformComponentMembers(hookBody) : hookBody;
|
|
970
1077
|
const isAsync = this.containsAwait(hookBody);
|
|
971
|
-
|
|
1078
|
+
let bodyCode = this.emitFunctionBody(transformed, params || []);
|
|
1079
|
+
bodyCode = this.addBodyRipSrcMarkers(bodyCode, hookBody);
|
|
972
1080
|
sl.push(` ${isAsync ? 'async ' : ''}${name}(${paramStr}) ${bodyCode}`);
|
|
973
1081
|
}
|
|
974
1082
|
}
|
|
@@ -981,6 +1089,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
981
1089
|
const sourceLines = this.options.source?.split('\n');
|
|
982
1090
|
const extractProps = (args) => {
|
|
983
1091
|
const props = [];
|
|
1092
|
+
const sideExprs = [];
|
|
984
1093
|
for (const arg of args) {
|
|
985
1094
|
let obj = null;
|
|
986
1095
|
if (this.is(arg, 'object')) {
|
|
@@ -995,8 +1104,26 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
995
1104
|
for (let j = 1; j < obj.length; j++) {
|
|
996
1105
|
const pair = obj[j];
|
|
997
1106
|
const [, key, value] = pair;
|
|
998
|
-
|
|
999
|
-
|
|
1107
|
+
const srcLine = pair.loc?.r ?? obj.loc?.r;
|
|
1108
|
+
// `@event` props on a component (parsed as `(. this name)`)
|
|
1109
|
+
// — not part of the declared prop type, but emit the value
|
|
1110
|
+
// as a bare expression so TS sees identifiers in the
|
|
1111
|
+
// handler body (hover, completion, identifier resolution).
|
|
1112
|
+
if (Array.isArray(key) && key[0] === '.' && key[1] === 'this') {
|
|
1113
|
+
try {
|
|
1114
|
+
const val = this.emitInComponent(value, 'value');
|
|
1115
|
+
sideExprs.push({ code: val, srcLine });
|
|
1116
|
+
} catch {}
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (typeof key === 'string' && key.startsWith('@')) {
|
|
1120
|
+
try {
|
|
1121
|
+
const val = this.emitInComponent(value, 'value');
|
|
1122
|
+
sideExprs.push({ code: val, srcLine });
|
|
1123
|
+
} catch {}
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof key === 'string') {
|
|
1000
1127
|
if (key.startsWith('__bind_') && key.endsWith('__')) {
|
|
1001
1128
|
// Two-way binding: emit the Signal object (this.xxx), not this.xxx.value
|
|
1002
1129
|
const member = typeof value === 'string' && this.reactiveMembers?.has(value) ? `this.${value}` : this.emitInComponent(value, 'value');
|
|
@@ -1009,10 +1136,25 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1009
1136
|
}
|
|
1010
1137
|
}
|
|
1011
1138
|
}
|
|
1139
|
+
props.sideExprs = sideExprs;
|
|
1012
1140
|
return props;
|
|
1013
1141
|
};
|
|
1014
|
-
const extractIntrinsicProps = (args) => {
|
|
1142
|
+
const extractIntrinsicProps = (args, tagName) => {
|
|
1015
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
|
+
};
|
|
1016
1158
|
for (const arg of args) {
|
|
1017
1159
|
let obj = null;
|
|
1018
1160
|
if (this.is(arg, 'object')) {
|
|
@@ -1049,7 +1191,8 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1049
1191
|
props.push({ code: `${propName}: ${val}`, srcLine });
|
|
1050
1192
|
} else {
|
|
1051
1193
|
const val = this.emitInComponent(value, 'value');
|
|
1052
|
-
|
|
1194
|
+
const finalVal = key === 'href' ? wrapHrefVal(val) : val;
|
|
1195
|
+
props.push({ code: `${key}: ${finalVal}`, srcLine });
|
|
1053
1196
|
}
|
|
1054
1197
|
}
|
|
1055
1198
|
}
|
|
@@ -1059,7 +1202,55 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1059
1202
|
};
|
|
1060
1203
|
const walkRender = (node) => {
|
|
1061
1204
|
if (!Array.isArray(node)) return;
|
|
1062
|
-
|
|
1205
|
+
let head = node[0]?.valueOf?.() ?? node[0];
|
|
1206
|
+
|
|
1207
|
+
// Tag shorthand normalization: `button.outline` is parsed as a
|
|
1208
|
+
// member-access s-expression `(. button outline)`. For render-block
|
|
1209
|
+
// type-checking we want to treat it as a regular tag node with a
|
|
1210
|
+
// string head `"button.outline"` so the existing intrinsic-tag
|
|
1211
|
+
// logic (which splits on `[.#]`) picks it up. Without this the
|
|
1212
|
+
// entire subtree (including @event handler bodies) is silently
|
|
1213
|
+
// skipped from the type-check stub.
|
|
1214
|
+
if (Array.isArray(head) && head[0] === '.') {
|
|
1215
|
+
const parts = [];
|
|
1216
|
+
const collect = (h) => {
|
|
1217
|
+
if (typeof h === 'string') { parts.push(h); return true; }
|
|
1218
|
+
if (Array.isArray(h) && h[0] === '.' && parts.length === 0) {
|
|
1219
|
+
if (!collect(h[1])) return false;
|
|
1220
|
+
if (typeof h[2] !== 'string' || !/^[\w-]+$/.test(h[2])) return false;
|
|
1221
|
+
parts.push('.', h[2]);
|
|
1222
|
+
return true;
|
|
1223
|
+
}
|
|
1224
|
+
return false;
|
|
1225
|
+
};
|
|
1226
|
+
if (collect(head) && parts.length >= 3 && /^[a-z][\w-]*$/.test(parts[0])) {
|
|
1227
|
+
const flat = parts.join('');
|
|
1228
|
+
const reshaped = [flat];
|
|
1229
|
+
for (let i = 1; i < node.length; i++) reshaped.push(node[i]);
|
|
1230
|
+
if (node.loc) reshaped.loc = node.loc;
|
|
1231
|
+
// Carry an _astNode back-reference so emitBareIdent can attach
|
|
1232
|
+
// anchors (used by recordSubMappings) on the original AST node
|
|
1233
|
+
// rather than this throwaway reshape, since recordSubMappings
|
|
1234
|
+
// walks the original tree.
|
|
1235
|
+
reshaped._astNode = node;
|
|
1236
|
+
// Strip .loc from the original tag-shorthand `(. tag class ...)`
|
|
1237
|
+
// chain so collectSubExprs (used by recordSubMappings) does not
|
|
1238
|
+
// anchor source identifiers (the class names) onto unrelated
|
|
1239
|
+
// gen-side property accesses with the same name. The class
|
|
1240
|
+
// segments (e.g. `image` in `div.image`) are CSS classes, not
|
|
1241
|
+
// identifier references, and should not participate in
|
|
1242
|
+
// identifier-name source-mapping heuristics.
|
|
1243
|
+
const stripLoc = (h) => {
|
|
1244
|
+
if (Array.isArray(h) && h[0] === '.') {
|
|
1245
|
+
if (h.loc) delete h.loc;
|
|
1246
|
+
stripLoc(h[1]);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
stripLoc(head);
|
|
1250
|
+
node = reshaped;
|
|
1251
|
+
head = flat;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1063
1254
|
|
|
1064
1255
|
// Object nodes are property bags (key-value pairs) — their values
|
|
1065
1256
|
// are code expressions (event handlers, bindings, literals), not
|
|
@@ -1164,13 +1355,43 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1164
1355
|
if (CodeEmitter.GENERATORS[child]) return;
|
|
1165
1356
|
if (child === 'null' || child === 'undefined' || child === 'true' || child === 'false') return;
|
|
1166
1357
|
let srcLine = parentNode.loc?.r;
|
|
1358
|
+
let srcCol = null;
|
|
1167
1359
|
if (srcLine != null && sourceLines) {
|
|
1168
|
-
const re = new RegExp(`\\b${child}\\b
|
|
1360
|
+
const re = new RegExp(`\\b${child}\\b`, 'g');
|
|
1169
1361
|
for (let ln = srcLine; ln < sourceLines.length; ln++) {
|
|
1170
|
-
|
|
1362
|
+
const lineText = sourceLines[ln];
|
|
1363
|
+
if (!lineText) continue;
|
|
1364
|
+
let searchFrom = (ln === parentNode.loc.r) ? (parentNode.loc.c + 1) : 0;
|
|
1365
|
+
re.lastIndex = searchFrom;
|
|
1366
|
+
let m;
|
|
1367
|
+
let found = -1;
|
|
1368
|
+
while ((m = re.exec(lineText)) !== null) {
|
|
1369
|
+
// Skip tag-shorthand class/id matches like `.error`/`#id`
|
|
1370
|
+
// (e.g. the `error` in `p.error error` — the bare-ident
|
|
1371
|
+
// child is the *second* `error`, not the CSS class).
|
|
1372
|
+
const prev = m.index > 0 ? lineText[m.index - 1] : '';
|
|
1373
|
+
if (prev === '.' || prev === '#') continue;
|
|
1374
|
+
found = m.index;
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
if (found >= 0) {
|
|
1378
|
+
srcLine = ln;
|
|
1379
|
+
srcCol = found;
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1171
1382
|
}
|
|
1172
1383
|
}
|
|
1173
1384
|
const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
|
|
1385
|
+
// Attach an anchor on the parent so collectSubExprs (used by
|
|
1386
|
+
// recordSubMappings) can record a precise source-map entry for
|
|
1387
|
+
// this bare identifier. Without this, source clicks/hovers on
|
|
1388
|
+
// the bare ident never resolve to the generated `this.X;` /
|
|
1389
|
+
// `X;` stub line, and the LSP can't route a semantic token here.
|
|
1390
|
+
if (srcLine != null && srcCol != null) {
|
|
1391
|
+
const anchorTarget = parentNode._astNode || parentNode;
|
|
1392
|
+
if (!anchorTarget._anchors) anchorTarget._anchors = [];
|
|
1393
|
+
anchorTarget._anchors.push({ name: child, origLine: srcLine, origCol: srcCol });
|
|
1394
|
+
}
|
|
1174
1395
|
if (this.componentMembers && this.componentMembers.has(child)) {
|
|
1175
1396
|
constructions.push(` this.${child};${srcMarker}`);
|
|
1176
1397
|
} else if (isTextChild) {
|
|
@@ -1185,7 +1406,13 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1185
1406
|
// Bare lowercase identifiers inside a block or as children of tag nodes
|
|
1186
1407
|
// — emit __ripEl so TS catches tag typos (e.g., slotz for slot), or
|
|
1187
1408
|
// emit this.prop for component member text references.
|
|
1188
|
-
|
|
1409
|
+
// Allow tag-shorthand heads like `div.image` or `button#submit` —
|
|
1410
|
+
// split on `.`/`#` and check the bare tag name. Without the
|
|
1411
|
+
// shorthand support here, expression children of `div.image x`
|
|
1412
|
+
// were silently dropped from the type-check stub even though the
|
|
1413
|
+
// lower `__ripEl` branch (which uses the same split) emitted the
|
|
1414
|
+
// tag itself.
|
|
1415
|
+
const isTagHead = typeof head === 'string' && /^[a-z][\w-]*(?:[.#][\w-]+)*$/.test(head) &&
|
|
1189
1416
|
!CodeEmitter.GENERATORS[head] && TEMPLATE_TAGS.has(head.split(/[.#]/)[0]);
|
|
1190
1417
|
if (head === 'block') {
|
|
1191
1418
|
for (let i = 1; i < node.length; i++) emitBareIdent(node[i], node, false);
|
|
@@ -1238,10 +1465,18 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1238
1465
|
constructions.push(` };`);
|
|
1239
1466
|
}
|
|
1240
1467
|
}
|
|
1468
|
+
// Emit @-event handler bodies as bare expression statements so
|
|
1469
|
+
// TS can resolve identifiers inside them (hover, completion).
|
|
1470
|
+
// They aren't part of the component's declared prop type.
|
|
1471
|
+
if (props.sideExprs) {
|
|
1472
|
+
for (const s of props.sideExprs) {
|
|
1473
|
+
constructions.push(` ${s.code};` + (s.srcLine != null ? ` // @rip-src:${s.srcLine}` : ''));
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1241
1476
|
} else if (typeof head === 'string' && !CodeEmitter.GENERATORS[head] && (TEMPLATE_TAGS.has(head.split(/[.#]/)[0]) ||
|
|
1242
1477
|
(/^[a-z][\w-]*$/.test(head) && node.length > 1))) {
|
|
1243
1478
|
const tagName = head.split(/[.#]/)[0];
|
|
1244
|
-
const iProps = extractIntrinsicProps(node.slice(1));
|
|
1479
|
+
const iProps = extractIntrinsicProps(node.slice(1), tagName);
|
|
1245
1480
|
const tagLine = node.loc?.r;
|
|
1246
1481
|
const srcMarker = tagLine != null ? ` // @rip-src:${tagLine}` : '';
|
|
1247
1482
|
if (iProps.length === 0) {
|
|
@@ -1309,7 +1544,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1309
1544
|
|
|
1310
1545
|
// State variables (__state handles signal passthrough)
|
|
1311
1546
|
for (const { name, value, isPublic, required } of stateVars) {
|
|
1312
|
-
if (isPublic && required) {
|
|
1547
|
+
if (isPublic && (required || value === undefined)) {
|
|
1313
1548
|
lines.push(` this.${name} = __state(props.__bind_${name}__ ?? props.${name});`);
|
|
1314
1549
|
} else if (isPublic) {
|
|
1315
1550
|
const val = this.emitInComponent(value, 'value');
|
|
@@ -1330,6 +1565,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1330
1565
|
lines.push(" if (!__k.startsWith('__bind_')) this._rest[__k] = props[__k];");
|
|
1331
1566
|
}
|
|
1332
1567
|
lines.push(' }');
|
|
1568
|
+
lines.push(' this.rest = __state(this._rest);');
|
|
1333
1569
|
}
|
|
1334
1570
|
|
|
1335
1571
|
// Computed (derived)
|
|
@@ -1349,17 +1585,17 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1349
1585
|
lines.push(` setContext('${name}', this.${name});`);
|
|
1350
1586
|
}
|
|
1351
1587
|
|
|
1352
|
-
// Effects
|
|
1588
|
+
// Effects (see comment on the parallel block above)
|
|
1353
1589
|
for (const effect of effects) {
|
|
1354
1590
|
const effectBody = effect[2];
|
|
1355
1591
|
const isAsync = this.containsAwait(effectBody) ? 'async ' : '';
|
|
1356
1592
|
if (this.is(effectBody, 'block')) {
|
|
1357
1593
|
const transformed = this.transformComponentMembers(effectBody);
|
|
1358
|
-
const body = this.emitFunctionBody(transformed
|
|
1594
|
+
const body = this.emitFunctionBody(transformed);
|
|
1359
1595
|
lines.push(` __effect(${isAsync}() => ${body});`);
|
|
1360
1596
|
} else {
|
|
1361
1597
|
const effectCode = this.emitInComponent(effectBody, 'value');
|
|
1362
|
-
lines.push(` __effect(${isAsync}() => { ${effectCode}; });`);
|
|
1598
|
+
lines.push(` __effect(${isAsync}() => { return ${effectCode}; });`);
|
|
1363
1599
|
}
|
|
1364
1600
|
}
|
|
1365
1601
|
|
|
@@ -1371,6 +1607,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1371
1607
|
lines.push(' this._rest || (this._rest = {});');
|
|
1372
1608
|
lines.push(' if (value == null) delete this._rest[key];');
|
|
1373
1609
|
lines.push(' else this._rest[key] = value;');
|
|
1610
|
+
lines.push(' this.rest.touch();');
|
|
1374
1611
|
lines.push(' this._applyInheritedProp(this._inheritedEl, key, value);');
|
|
1375
1612
|
lines.push(' }');
|
|
1376
1613
|
lines.push(' _applyRestToInheritedEl() {');
|
|
@@ -1539,6 +1776,8 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1539
1776
|
this._loopVarStack = [];
|
|
1540
1777
|
this._factoryMode = false;
|
|
1541
1778
|
this._factoryVars = null;
|
|
1779
|
+
this._renderLocalScope = new Set();
|
|
1780
|
+
this._renderTopLocals = new Set(); // class-mode (top-level _create) hoisted lets
|
|
1542
1781
|
this._fragChildren = new Map();
|
|
1543
1782
|
this._pendingAutoWire = false;
|
|
1544
1783
|
this._autoWireEl = null;
|
|
@@ -1547,25 +1786,52 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1547
1786
|
|
|
1548
1787
|
const statements = this.is(body, 'block') ? body.slice(1) : [body];
|
|
1549
1788
|
|
|
1789
|
+
// Pre-count renderable (non-binding) statements so the single-renderable
|
|
1790
|
+
// optimization survives the presence of leading binding statements like
|
|
1791
|
+
// `code = expr`. Bindings are emitted into _createLines as JS but
|
|
1792
|
+
// produce no DOM child to append.
|
|
1793
|
+
const renderableCount = statements.reduce(
|
|
1794
|
+
(n, s) => n + (this._isRenderBinding(s) ? 0 : 1), 0);
|
|
1795
|
+
|
|
1550
1796
|
let rootVar;
|
|
1551
1797
|
if (statements.length === 0 || (statements.length === 1 && statements[0] === 'null')) {
|
|
1552
1798
|
rootVar = 'null';
|
|
1553
|
-
} else if (
|
|
1799
|
+
} else if (renderableCount === 0) {
|
|
1800
|
+
// All statements are bindings — emit them and return a comment placeholder
|
|
1801
|
+
for (const stmt of statements) this.emitNode(stmt);
|
|
1802
|
+
rootVar = this.newElementVar('empty');
|
|
1803
|
+
this._createLines.push(`${rootVar} = document.createComment('');`);
|
|
1804
|
+
} else if (renderableCount === 1) {
|
|
1554
1805
|
this._pendingAutoWire = !!this._autoEventHandlers;
|
|
1555
|
-
|
|
1806
|
+
let onlyRenderable = null;
|
|
1807
|
+
for (const stmt of statements) {
|
|
1808
|
+
const v = this.emitNode(stmt);
|
|
1809
|
+
if (v != null) onlyRenderable = v;
|
|
1810
|
+
}
|
|
1556
1811
|
this._pendingAutoWire = false;
|
|
1812
|
+
rootVar = onlyRenderable;
|
|
1557
1813
|
} else {
|
|
1558
1814
|
rootVar = this.newElementVar('frag');
|
|
1559
1815
|
this._createLines.push(`${rootVar} = document.createDocumentFragment();`);
|
|
1560
1816
|
const children = [];
|
|
1561
1817
|
for (const stmt of statements) {
|
|
1562
1818
|
const childVar = this.emitNode(stmt);
|
|
1819
|
+
if (childVar == null) continue;
|
|
1563
1820
|
this._createLines.push(`${rootVar}.appendChild(${childVar});`);
|
|
1564
1821
|
children.push(childVar);
|
|
1565
1822
|
}
|
|
1566
1823
|
this._fragChildren.set(rootVar, children);
|
|
1567
1824
|
}
|
|
1568
1825
|
|
|
1826
|
+
// Hoist class-mode render-local declarations to the top of _create() so
|
|
1827
|
+
// a) duplicate `name = ...` statements in source don't generate `let name`
|
|
1828
|
+
// twice (strict-mode redeclaration error), and b) reads inside any
|
|
1829
|
+
// enclosing IIFE/closure can see the binding.
|
|
1830
|
+
if (this._renderTopLocals.size > 0) {
|
|
1831
|
+
const decl = `let ${[...this._renderTopLocals].join(', ')};`;
|
|
1832
|
+
this._createLines.unshift(decl);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1569
1835
|
return {
|
|
1570
1836
|
createLines: this._createLines,
|
|
1571
1837
|
setupLines: this._setupLines,
|
|
@@ -1598,20 +1864,88 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1598
1864
|
get() { return this._factoryMode ? 'ctx' : 'this'; }
|
|
1599
1865
|
});
|
|
1600
1866
|
|
|
1601
|
-
/** Push an effect line, wrapping with disposer tracking in factory mode
|
|
1867
|
+
/** Push an effect line, wrapping with disposer tracking in factory mode.
|
|
1868
|
+
*
|
|
1869
|
+
* Factory effects opt out of the runtime's auto-registration with the
|
|
1870
|
+
* current component (skipRegister:true). The factory's own `disposers`
|
|
1871
|
+
* array owns these — they're already cleaned up by d(detaching) when
|
|
1872
|
+
* the block is removed. Without skipRegister, every block re-render
|
|
1873
|
+
* would leak a stale disposer onto the parent's _disposers (the local
|
|
1874
|
+
* factory disposers get overwritten on each p() call, but the parent
|
|
1875
|
+
* keeps the stale references until parent unmount). */
|
|
1602
1876
|
proto._pushEffect = function(body) {
|
|
1603
1877
|
if (this._factoryMode) {
|
|
1604
|
-
this._setupLines.push(`disposers.push(__effect(() => { ${body} }));`);
|
|
1878
|
+
this._setupLines.push(`disposers.push(__effect(() => { ${body} }, {skipRegister: true}));`);
|
|
1605
1879
|
} else {
|
|
1606
1880
|
this._setupLines.push(`__effect(() => { ${body} });`);
|
|
1607
1881
|
}
|
|
1608
1882
|
};
|
|
1609
1883
|
|
|
1884
|
+
// --------------------------------------------------------------------------
|
|
1885
|
+
// Render-scope locals — declarations like `code = expr` inside render
|
|
1886
|
+
// --------------------------------------------------------------------------
|
|
1887
|
+
// Tracked per-factory so emitNode can (a) emit `code = expr` as a real JS
|
|
1888
|
+
// local instead of a text node, and (b) treat subsequent `span code`
|
|
1889
|
+
// references as value reads instead of `<code>` element emissions. Each
|
|
1890
|
+
// block factory (loop body, conditional branch) is its own JS function,
|
|
1891
|
+
// so render locals don't cross factory boundaries; only loop vars do
|
|
1892
|
+
// (threaded via positional parameters from _loopVarStack).
|
|
1893
|
+
|
|
1894
|
+
const _isPlainIdentifier = (s) =>
|
|
1895
|
+
typeof s === 'string' && /^[A-Za-z_$][\w$]*$/.test(s);
|
|
1896
|
+
|
|
1897
|
+
// Assignment-shape heads that emit as JS statements at render-block top
|
|
1898
|
+
// level instead of being wrapped in createTextNode. `=` declares a new
|
|
1899
|
+
// local; compound forms mutate an existing one.
|
|
1900
|
+
const _ASSIGN_HEADS = new Set([
|
|
1901
|
+
'=', '+=', '-=', '*=', '/=', '%=', '**=',
|
|
1902
|
+
'&&=', '||=', '?=', '??=',
|
|
1903
|
+
]);
|
|
1904
|
+
|
|
1905
|
+
proto._isRenderBinding = function(stmt) {
|
|
1906
|
+
return Array.isArray(stmt) && _ASSIGN_HEADS.has(stmt[0]) && _isPlainIdentifier(stmt[1]);
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1909
|
+
proto._addRenderLocal = function(name) {
|
|
1910
|
+
if (this._renderLocalScope) this._renderLocalScope.add(name);
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
proto._isRenderLocal = function(name) {
|
|
1914
|
+
if (!name || typeof name !== 'string') return false;
|
|
1915
|
+
if (this._renderLocalScope && this._renderLocalScope.has(name)) return true;
|
|
1916
|
+
if (this._loopVarStack) {
|
|
1917
|
+
// Loop vars ARE threaded across nested factories as positional
|
|
1918
|
+
// parameters, so any ancestor loop var is in scope inside any
|
|
1919
|
+
// descendant factory function.
|
|
1920
|
+
for (const v of this._loopVarStack) {
|
|
1921
|
+
if (v.itemVar === name || v.indexVar === name) return true;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return false;
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1610
1927
|
// --------------------------------------------------------------------------
|
|
1611
1928
|
// emitNode — main dispatch for all render tree nodes
|
|
1612
1929
|
// --------------------------------------------------------------------------
|
|
1613
1930
|
|
|
1614
1931
|
proto.emitNode = function(sexpr) {
|
|
1932
|
+
// Render-scope assignment — `code = expr` and compound forms (`+=`,
|
|
1933
|
+
// `-=`, etc.) become JS statements at this position rather than text
|
|
1934
|
+
// nodes. Returning null tells the caller "no DOM child to append".
|
|
1935
|
+
// `=` also declares the local: hoisted via _factoryVars (factory mode)
|
|
1936
|
+
// or _renderTopLocals (class mode) and added to _renderLocalScope so
|
|
1937
|
+
// subsequent references resolve to the local instead of an HTML tag.
|
|
1938
|
+
if (this._isRenderBinding(sexpr)) {
|
|
1939
|
+
const [op, name, expr] = sexpr;
|
|
1940
|
+
const exprCode = this.emitInComponent(expr, 'value');
|
|
1941
|
+
if (op === '=') {
|
|
1942
|
+
(this._factoryMode ? this._factoryVars : this._renderTopLocals).add(name);
|
|
1943
|
+
this._addRenderLocal(name);
|
|
1944
|
+
}
|
|
1945
|
+
this._createLines.push(`${name} ${op} ${exprCode};`);
|
|
1946
|
+
return null;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1615
1949
|
// String literal → text node (handle both primitive and String objects)
|
|
1616
1950
|
if (typeof sexpr === 'string' || sexpr instanceof String) {
|
|
1617
1951
|
const str = sexpr.valueOf();
|
|
@@ -1627,6 +1961,13 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1627
1961
|
this._pushEffect(`${textVar}.data = ${this._self}.${str}.value;`);
|
|
1628
1962
|
return textVar;
|
|
1629
1963
|
}
|
|
1964
|
+
// Render-scope local (binding from `code = expr` or a loop var) — emit
|
|
1965
|
+
// as a value read, NOT a tag. Lexical bindings shadow HTML tag names.
|
|
1966
|
+
if (this._isRenderLocal(str)) {
|
|
1967
|
+
const textVar = this.newTextVar();
|
|
1968
|
+
this._createLines.push(`${textVar} = document.createTextNode(String(${str}));`);
|
|
1969
|
+
return textVar;
|
|
1970
|
+
}
|
|
1630
1971
|
// Slot projection — bare <slot> tag → project @children
|
|
1631
1972
|
if (str === 'slot' && this.componentMembers) {
|
|
1632
1973
|
const s = this._self;
|
|
@@ -1697,8 +2038,14 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1697
2038
|
return cv;
|
|
1698
2039
|
}
|
|
1699
2040
|
|
|
1700
|
-
// HTML tag (possibly with #id, e.g. div#content)
|
|
1701
|
-
|
|
2041
|
+
// HTML tag (possibly with #id, e.g. div#content). A render-local with
|
|
2042
|
+
// the same name as an HTML tag wins — `code "hi"` after `code = fn`
|
|
2043
|
+
// is a function call, not a `<code>` element. Checking the full
|
|
2044
|
+
// headStr (not just the part before `#id`) means `div#main` naturally
|
|
2045
|
+
// dispatches as a tag even when a `div` local exists, since locals
|
|
2046
|
+
// are plain identifiers and can't contain `#`.
|
|
2047
|
+
if (headStr && this.isHtmlTag(headStr) && !meta(head, 'text') &&
|
|
2048
|
+
!this._isRenderLocal(headStr)) {
|
|
1702
2049
|
let [tagName, id] = headStr.split('#');
|
|
1703
2050
|
return this.emitTag(tagName || 'div', [], rest, id);
|
|
1704
2051
|
}
|
|
@@ -1721,9 +2068,11 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1721
2068
|
return slotVar;
|
|
1722
2069
|
}
|
|
1723
2070
|
|
|
1724
|
-
// HTML tag with classes (div.class) — skip if base is marked .text by
|
|
2071
|
+
// HTML tag with classes (div.class) — skip if base is marked .text by
|
|
2072
|
+
// = prefix, and skip if the root is a render-local (so `code.value`
|
|
2073
|
+
// after `code = obj` reads obj.value, not <code class="value">).
|
|
1725
2074
|
const { tag, classes, id, base } = this.collectTemplateClasses(sexpr);
|
|
1726
|
-
if (!meta(base, 'text') && tag && this.isHtmlTag(tag)) {
|
|
2075
|
+
if (!meta(base, 'text') && tag && this.isHtmlTag(tag) && !this._isRenderLocal(tag)) {
|
|
1727
2076
|
return this.emitTag(tag, classes, [], id);
|
|
1728
2077
|
}
|
|
1729
2078
|
|
|
@@ -1754,7 +2103,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1754
2103
|
}
|
|
1755
2104
|
|
|
1756
2105
|
const { tag, classes, id } = this.collectTemplateClasses(head);
|
|
1757
|
-
if (tag && this.isHtmlTag(tag)) {
|
|
2106
|
+
if (tag && this.isHtmlTag(tag) && !this._isRenderLocal(tag)) {
|
|
1758
2107
|
// Dynamic class syntax: div.("classes") or div.card.("classes")
|
|
1759
2108
|
if (classes.length > 0 && classes[classes.length - 1] === '__clsx') {
|
|
1760
2109
|
const staticClasses = classes.slice(0, -1);
|
|
@@ -1820,12 +2169,15 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1820
2169
|
this.emitAttributes(elVar, child);
|
|
1821
2170
|
} else {
|
|
1822
2171
|
const childVar = this.emitNode(child);
|
|
2172
|
+
if (childVar == null) continue;
|
|
1823
2173
|
this._createLines.push(`${elVar}.appendChild(${childVar});`);
|
|
1824
2174
|
}
|
|
1825
2175
|
}
|
|
1826
2176
|
} else if (block) {
|
|
1827
2177
|
const childVar = this.emitNode(block);
|
|
1828
|
-
|
|
2178
|
+
if (childVar != null) {
|
|
2179
|
+
this._createLines.push(`${elVar}.appendChild(${childVar});`);
|
|
2180
|
+
}
|
|
1829
2181
|
}
|
|
1830
2182
|
}
|
|
1831
2183
|
else if (this.is(arg, 'object')) {
|
|
@@ -1833,9 +2185,15 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1833
2185
|
}
|
|
1834
2186
|
else if (typeof arg === 'string' || arg instanceof String) {
|
|
1835
2187
|
const val = arg.valueOf();
|
|
1836
|
-
// Template tag appearing as a string arg (e.g., slot after multi-line attrs)
|
|
2188
|
+
// Template tag appearing as a string arg (e.g., slot after multi-line attrs).
|
|
2189
|
+
// Render-scope locals (and loop vars) take precedence over the HTML tag
|
|
2190
|
+
// sugar — `for code in items \n span code` should mean text content,
|
|
2191
|
+
// not a nested <code> element. JSX-equivalent: if `code` is a let
|
|
2192
|
+
// binding in scope, `<span>{code}</span>`, never `<span><code/></span>`.
|
|
2193
|
+
// (Render-local names are plain identifiers — no `#id` or `.class`
|
|
2194
|
+
// tail can ever be one — so checking the base name alone is enough.)
|
|
1837
2195
|
const baseName = val.split(/[#.]/)[0];
|
|
1838
|
-
if (this.isHtmlTag(baseName || 'div') || this.isComponent(baseName)) {
|
|
2196
|
+
if (!this._isRenderLocal(baseName) && (this.isHtmlTag(baseName || 'div') || this.isComponent(baseName))) {
|
|
1839
2197
|
const childVar = this.emitNode(arg);
|
|
1840
2198
|
this._createLines.push(`${elVar}.appendChild(${childVar});`);
|
|
1841
2199
|
} else {
|
|
@@ -1855,7 +2213,9 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1855
2213
|
}
|
|
1856
2214
|
else if (arg) {
|
|
1857
2215
|
const childVar = this.emitNode(arg);
|
|
1858
|
-
|
|
2216
|
+
if (childVar != null) {
|
|
2217
|
+
this._createLines.push(`${elVar}.appendChild(${childVar});`);
|
|
2218
|
+
}
|
|
1859
2219
|
}
|
|
1860
2220
|
}
|
|
1861
2221
|
};
|
|
@@ -2131,7 +2491,14 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2131
2491
|
|
|
2132
2492
|
proto.emitTemplateBlock = function(body) {
|
|
2133
2493
|
if (!Array.isArray(body) || body[0] !== 'block') {
|
|
2134
|
-
|
|
2494
|
+
const v = this.emitNode(body);
|
|
2495
|
+
if (v != null) return v;
|
|
2496
|
+
// Lone binding (e.g., `code = expr`) at the position where a child
|
|
2497
|
+
// node was expected — emit a placeholder so the caller has something
|
|
2498
|
+
// to insert. The binding itself was already pushed into _createLines.
|
|
2499
|
+
const commentVar = this.newElementVar('empty');
|
|
2500
|
+
this._createLines.push(`${commentVar} = document.createComment('');`);
|
|
2501
|
+
return commentVar;
|
|
2135
2502
|
}
|
|
2136
2503
|
|
|
2137
2504
|
const statements = body.slice(1);
|
|
@@ -2140,8 +2507,24 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2140
2507
|
this._createLines.push(`${commentVar} = document.createComment('');`);
|
|
2141
2508
|
return commentVar;
|
|
2142
2509
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2510
|
+
|
|
2511
|
+
const renderableCount = statements.reduce(
|
|
2512
|
+
(n, s) => n + (this._isRenderBinding(s) ? 0 : 1), 0);
|
|
2513
|
+
|
|
2514
|
+
if (renderableCount === 0) {
|
|
2515
|
+
for (const stmt of statements) this.emitNode(stmt);
|
|
2516
|
+
const commentVar = this.newElementVar('empty');
|
|
2517
|
+
this._createLines.push(`${commentVar} = document.createComment('');`);
|
|
2518
|
+
return commentVar;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
if (renderableCount === 1) {
|
|
2522
|
+
let only = null;
|
|
2523
|
+
for (const stmt of statements) {
|
|
2524
|
+
const v = this.emitNode(stmt);
|
|
2525
|
+
if (v != null) only = v;
|
|
2526
|
+
}
|
|
2527
|
+
return only;
|
|
2145
2528
|
}
|
|
2146
2529
|
|
|
2147
2530
|
const fragVar = this.newElementVar('frag');
|
|
@@ -2149,6 +2532,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2149
2532
|
const children = [];
|
|
2150
2533
|
for (const stmt of statements) {
|
|
2151
2534
|
const childVar = this.emitNode(stmt);
|
|
2535
|
+
if (childVar == null) continue;
|
|
2152
2536
|
this._createLines.push(`${fragVar}.appendChild(${childVar});`);
|
|
2153
2537
|
children.push(childVar);
|
|
2154
2538
|
}
|
|
@@ -2199,8 +2583,12 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2199
2583
|
setupLines.push(` const anchor = ${anchorVar};`);
|
|
2200
2584
|
setupLines.push(` let currentBlock = null;`);
|
|
2201
2585
|
setupLines.push(` let showing = null;`);
|
|
2586
|
+
// Factory effects skip auto-registration on the parent component
|
|
2587
|
+
// (their disposers live in the local `disposers` array and are
|
|
2588
|
+
// cleaned up by d(detaching)). Class-mode effects auto-register
|
|
2589
|
+
// on `this` via the runtime's __getCurrentComponent bridge.
|
|
2202
2590
|
const effOpen = this._factoryMode ? 'disposers.push(__effect(() => {' : '__effect(() => {';
|
|
2203
|
-
const effClose = this._factoryMode ? '}));' : '});';
|
|
2591
|
+
const effClose = this._factoryMode ? '}, {skipRegister: true}));' : '});';
|
|
2204
2592
|
setupLines.push(` ${effOpen}`);
|
|
2205
2593
|
setupLines.push(` const show = !!(${condCode});`);
|
|
2206
2594
|
setupLines.push(` const want = show ? 'then' : ${elseBlock ? "'else'" : 'null'};`);
|
|
@@ -2231,8 +2619,19 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2231
2619
|
setupLines.push(` }`);
|
|
2232
2620
|
}
|
|
2233
2621
|
setupLines.push(` ${effClose}`);
|
|
2622
|
+
// Block teardown: when this conditional's enclosing scope ends
|
|
2623
|
+
// (factory block detach, or parent component unmount), destroy
|
|
2624
|
+
// the currentBlock so its DOM, effects, and child components
|
|
2625
|
+
// are fully cleaned up. Without this, parent unmount would
|
|
2626
|
+
// dispose the reactive effect (preventing future re-runs) but
|
|
2627
|
+
// leave currentBlock alive — its child components, signal
|
|
2628
|
+
// subscriptions, and detached DOM stay pinned in memory.
|
|
2234
2629
|
if (this._factoryMode) {
|
|
2235
2630
|
setupLines.push(` disposers.push(() => { if (currentBlock) { currentBlock.d(true); currentBlock = null; } });`);
|
|
2631
|
+
} else {
|
|
2632
|
+
// Class mode: register on parent component's _disposers via the
|
|
2633
|
+
// __getCurrentComponent bridge (the same pathway __effect uses).
|
|
2634
|
+
setupLines.push(` { const __cur = globalThis.__ripComponent?.__getCurrentComponent?.(); if (__cur) (__cur._disposers ??= []).push(() => { if (currentBlock) { currentBlock.d(true); currentBlock = null; } }); }`);
|
|
2236
2635
|
}
|
|
2237
2636
|
setupLines.push(`}`);
|
|
2238
2637
|
|
|
@@ -2246,19 +2645,21 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2246
2645
|
// --------------------------------------------------------------------------
|
|
2247
2646
|
|
|
2248
2647
|
proto.emitConditionBranch = function(blockName, block) {
|
|
2249
|
-
const saved = [this._createLines, this._setupLines, this._factoryMode, this._factoryVars];
|
|
2648
|
+
const saved = [this._createLines, this._setupLines, this._factoryMode, this._factoryVars, this._renderLocalScope];
|
|
2250
2649
|
|
|
2251
2650
|
this._createLines = [];
|
|
2252
2651
|
this._setupLines = [];
|
|
2253
2652
|
this._factoryMode = true;
|
|
2254
2653
|
this._factoryVars = new Set();
|
|
2654
|
+
// Fresh render-local scope per factory function (see emitTemplateLoop).
|
|
2655
|
+
this._renderLocalScope = new Set();
|
|
2255
2656
|
|
|
2256
2657
|
const rootVar = this.emitTemplateBlock(block);
|
|
2257
2658
|
const createLines = this._createLines;
|
|
2258
2659
|
const setupLines = this._setupLines;
|
|
2259
2660
|
const factoryVars = this._factoryVars;
|
|
2260
2661
|
|
|
2261
|
-
[this._createLines, this._setupLines, this._factoryMode, this._factoryVars] = saved;
|
|
2662
|
+
[this._createLines, this._setupLines, this._factoryMode, this._factoryVars, this._renderLocalScope] = saved;
|
|
2262
2663
|
|
|
2263
2664
|
const outerParams = this._loopVarStack.map(v => `${v.itemVar}, ${v.indexVar}`).join(', ');
|
|
2264
2665
|
const extraParams = outerParams ? `, ${outerParams}` : '';
|
|
@@ -2282,6 +2683,15 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2282
2683
|
if (hasEffects) {
|
|
2283
2684
|
factoryLines.push(` let disposers = [];`);
|
|
2284
2685
|
}
|
|
2686
|
+
// Per-factory list of child-component instances created inside this
|
|
2687
|
+
// block. d(detaching) calls .unmount() on each so child lifecycle
|
|
2688
|
+
// hooks fire and child effects get cleaned up when the block is
|
|
2689
|
+
// removed by reactive rendering (a `for` loop iteration changing,
|
|
2690
|
+
// an `if` branch flipping, etc.). Without this, child instances
|
|
2691
|
+
// were only ever unmounted when the OUTER component itself died,
|
|
2692
|
+
// which silently broke the `beforeUnmount` / `unmounted` contract
|
|
2693
|
+
// for any child rendered inside a conditional or loop.
|
|
2694
|
+
factoryLines.push(` let _factoryChildren = [];`);
|
|
2285
2695
|
|
|
2286
2696
|
factoryLines.push(` return {`);
|
|
2287
2697
|
|
|
@@ -2320,6 +2730,12 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2320
2730
|
factoryLines.push(` },`);
|
|
2321
2731
|
|
|
2322
2732
|
factoryLines.push(` d(detaching) {`);
|
|
2733
|
+
// Unmount any child components inside this block FIRST, so their
|
|
2734
|
+
// lifecycle hooks and effect disposers fire before we drop our own
|
|
2735
|
+
// disposers and DOM. removeDOM:false because the parent DOM removal
|
|
2736
|
+
// (below) will detach the whole subtree in one shot.
|
|
2737
|
+
factoryLines.push(` for (const __c of _factoryChildren) { try { __c.unmount?.({removeDOM: false}); } catch (__e) { console.error('[Rip] factory child unmount error:', __e); } }`);
|
|
2738
|
+
factoryLines.push(` _factoryChildren = [];`);
|
|
2323
2739
|
if (hasEffects) {
|
|
2324
2740
|
factoryLines.push(` disposers.forEach(d => d());`);
|
|
2325
2741
|
}
|
|
@@ -2355,12 +2771,27 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2355
2771
|
const itemVar = varNames[0];
|
|
2356
2772
|
let indexVar = varNames[1] || null;
|
|
2357
2773
|
if (!indexVar) {
|
|
2774
|
+
// Pick a name that won't collide with:
|
|
2775
|
+
// * outer loop vars (already in _loopVarStack)
|
|
2776
|
+
// * the current item var
|
|
2777
|
+
// * any explicit `for` var or render-local declaration ANYWHERE in
|
|
2778
|
+
// the body subtree (any depth, through conditionals and nested
|
|
2779
|
+
// loops alike)
|
|
2780
|
+
// The third check is what closes the "duplicate-name" family of
|
|
2781
|
+
// strict-mode errors: the outer loop's auto-index becomes a
|
|
2782
|
+
// positional parameter of every nested factory's patch function, so
|
|
2783
|
+
// any `let i;` (from `i = ...`) or any explicit `for v, i in ...`
|
|
2784
|
+
// anywhere inside would clash with an auto-allocated `i`.
|
|
2358
2785
|
const usedNames = new Set(this._loopVarStack.flatMap(v => [v.itemVar, v.indexVar]));
|
|
2359
2786
|
usedNames.add(itemVar);
|
|
2787
|
+
this._collectBodyBindings(body, usedNames);
|
|
2360
2788
|
for (const candidate of ['i', 'j', 'k', 'l', 'm', 'n']) {
|
|
2361
2789
|
if (!usedNames.has(candidate)) { indexVar = candidate; break; }
|
|
2362
2790
|
}
|
|
2363
|
-
|
|
2791
|
+
// Last-resort fallback uses a mangled name no normal user identifier
|
|
2792
|
+
// collides with (double underscore prefix matches the convention used
|
|
2793
|
+
// by other compiler-internal names like __ripComponent, __reconcile).
|
|
2794
|
+
indexVar = indexVar || `__rip_idx${this._loopVarStack.length}`;
|
|
2364
2795
|
}
|
|
2365
2796
|
|
|
2366
2797
|
const collectionCode = this.emitInComponent(collection, 'value');
|
|
@@ -2385,24 +2816,34 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2385
2816
|
}
|
|
2386
2817
|
}
|
|
2387
2818
|
|
|
2388
|
-
const saved = [this._createLines, this._setupLines, this._factoryMode, this._factoryVars];
|
|
2819
|
+
const saved = [this._createLines, this._setupLines, this._factoryMode, this._factoryVars, this._renderLocalScope];
|
|
2389
2820
|
|
|
2390
2821
|
this._createLines = [];
|
|
2391
2822
|
this._setupLines = [];
|
|
2392
2823
|
this._factoryMode = true;
|
|
2393
2824
|
this._factoryVars = new Set();
|
|
2825
|
+
// Loop body is its own JS factory function — render locals from the
|
|
2826
|
+
// surrounding scope are NOT visible here. Loop vars are explicitly
|
|
2827
|
+
// threaded as positional parameters via _loopVarStack.
|
|
2828
|
+
this._renderLocalScope = new Set();
|
|
2394
2829
|
|
|
2395
2830
|
const outerParams = this._loopVarStack.map(v => `${v.itemVar}, ${v.indexVar}`).join(', ');
|
|
2396
2831
|
const outerExtra = outerParams ? `, ${outerParams}` : '';
|
|
2397
2832
|
|
|
2398
|
-
|
|
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 });
|
|
2399
2840
|
const itemNode = this.emitTemplateBlock(body);
|
|
2400
2841
|
this._loopVarStack.pop();
|
|
2401
2842
|
const itemCreateLines = this._createLines;
|
|
2402
2843
|
const itemSetupLines = this._setupLines;
|
|
2403
2844
|
const itemFactoryVars = this._factoryVars;
|
|
2404
2845
|
|
|
2405
|
-
[this._createLines, this._setupLines, this._factoryMode, this._factoryVars] = saved;
|
|
2846
|
+
[this._createLines, this._setupLines, this._factoryMode, this._factoryVars, this._renderLocalScope] = saved;
|
|
2406
2847
|
|
|
2407
2848
|
const isStatic = itemSetupLines.length === 0;
|
|
2408
2849
|
const loopParams = `ctx, ${itemVar}, ${indexVar}${outerExtra}`;
|
|
@@ -2420,11 +2861,23 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2420
2861
|
setupLines.push(`// Loop: ${blockName}`);
|
|
2421
2862
|
setupLines.push(`{`);
|
|
2422
2863
|
setupLines.push(` const __s = { blocks: [], keys: [] };`);
|
|
2864
|
+
// Same skipRegister contract as the conditional emitter above.
|
|
2423
2865
|
const effOpen = this._factoryMode ? 'disposers.push(__effect(() => {' : '__effect(() => {';
|
|
2424
|
-
const effClose = this._factoryMode ? '}));' : '});';
|
|
2866
|
+
const effClose = this._factoryMode ? '}, {skipRegister: true}));' : '});';
|
|
2425
2867
|
setupLines.push(` ${effOpen}`);
|
|
2426
2868
|
setupLines.push(` __reconcile(${anchorVar}, __s, ${collectionCode}, ${this._self}, ${blockName}, ${keyFnCode}${outerArgs});`);
|
|
2427
2869
|
setupLines.push(` ${effClose}`);
|
|
2870
|
+
// Loop teardown: destroy every block in state.blocks on parent
|
|
2871
|
+
// unmount (or enclosing factory detach). __reconcile only destroys
|
|
2872
|
+
// blocks that are removed mid-render; blocks that exist at the
|
|
2873
|
+
// time the parent dies would otherwise leak their DOM, child
|
|
2874
|
+
// components, and effect subscriptions. Same shape as the
|
|
2875
|
+
// conditional emitter's block-teardown disposer.
|
|
2876
|
+
if (this._factoryMode) {
|
|
2877
|
+
setupLines.push(` disposers.push(() => { for (const __b of __s.blocks) { try { __b.d(true); } catch {} } __s.blocks = []; __s.keys = []; __s.items = []; });`);
|
|
2878
|
+
} else {
|
|
2879
|
+
setupLines.push(` { const __cur = globalThis.__ripComponent?.__getCurrentComponent?.(); if (__cur) (__cur._disposers ??= []).push(() => { for (const __b of __s.blocks) { try { __b.d(true); } catch {} } __s.blocks = []; __s.keys = []; __s.items = []; }); }`);
|
|
2880
|
+
}
|
|
2428
2881
|
setupLines.push(`}`);
|
|
2429
2882
|
|
|
2430
2883
|
this._setupLines.push(setupLines.join('\n '));
|
|
@@ -2443,21 +2896,97 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2443
2896
|
const { propsCode, reactiveProps, eventBindings, childrenSetupLines } = this.buildComponentProps(args);
|
|
2444
2897
|
|
|
2445
2898
|
const s = this._self;
|
|
2899
|
+
// Push parent (s) so its constructor-time __pushComponent stack is
|
|
2900
|
+
// preserved across child instantiation. Then push the CHILD around
|
|
2901
|
+
// its _create() / _setup() calls so any __effect those methods
|
|
2902
|
+
// create auto-registers on the child's _disposers, not the parent's.
|
|
2903
|
+
//
|
|
2904
|
+
// Partial-construction failure handling. Two cases produce a broken
|
|
2905
|
+
// child whose _create/_setup would crash:
|
|
2906
|
+
// 1. _init throws and a parent onError boundary handles it. The
|
|
2907
|
+
// constructor sets ._initFailed and returns the (broken)
|
|
2908
|
+
// instance. We detect via the flag.
|
|
2909
|
+
// 2. _init throws and no boundary handles it. __handleComponentError
|
|
2910
|
+
// re-throws from the constructor; OR _create itself throws
|
|
2911
|
+
// after a successful _init. The outer try/catch handles both.
|
|
2912
|
+
// In all failure modes we substitute a comment-node placeholder so
|
|
2913
|
+
// the parent's appendChild later still finds a valid node, log the
|
|
2914
|
+
// error, and don't push the broken instance onto _children (so the
|
|
2915
|
+
// unmount cascade doesn't walk into it).
|
|
2446
2916
|
this._createLines.push(`{ const __prev = __pushComponent(${s}); try {`);
|
|
2917
|
+
this._createLines.push(`try {`);
|
|
2447
2918
|
this._createLines.push(`${instVar} = new ${componentName}(${propsCode});`);
|
|
2448
|
-
this._createLines.push(
|
|
2449
|
-
|
|
2919
|
+
this._createLines.push(`if (${instVar} && ${instVar}._initFailed) {`);
|
|
2920
|
+
// _init may have registered effects, contexts, or sub-children
|
|
2921
|
+
// before throwing. unmount({removeDOM:false}) releases them; the
|
|
2922
|
+
// instance never reached a usable state but its partial side
|
|
2923
|
+
// effects need cleanup. Idempotent so safe even when nothing was
|
|
2924
|
+
// registered yet.
|
|
2925
|
+
this._createLines.push(` try { ${instVar}.unmount({removeDOM: false}); } catch (__ue) { console.error('[Rip] partial-init unmount error:', __ue); }`);
|
|
2926
|
+
this._createLines.push(` ${instVar} = null;`);
|
|
2927
|
+
this._createLines.push(` ${elVar} = document.createComment('rip:child-init-failed: ${componentName}');`);
|
|
2928
|
+
this._createLines.push(`} else {`);
|
|
2929
|
+
this._createLines.push(` { const __cprev = __pushComponent(${instVar}); try {`);
|
|
2930
|
+
this._createLines.push(` ${elVar} = ${instVar}._root = ${instVar}._create();`);
|
|
2931
|
+
this._createLines.push(` } finally { __popComponent(__cprev); } }`);
|
|
2932
|
+
if (this._factoryMode) {
|
|
2933
|
+
// Factory blocks (for/if in render) own their child instances
|
|
2934
|
+
// exclusively. Don't pin them on the PARENT's _children — that
|
|
2935
|
+
// array would grow unboundedly on loop churn (every removed
|
|
2936
|
+
// iteration would leave a stale reference). The factory's
|
|
2937
|
+
// d(detaching) iterates _factoryChildren and unmounts them; on
|
|
2938
|
+
// parent unmount, the parent's own disposer chain destroys the
|
|
2939
|
+
// factory block, which cascades to these children via d().
|
|
2940
|
+
this._createLines.push(` _factoryChildren.push(${instVar});`);
|
|
2941
|
+
} else {
|
|
2942
|
+
// Class-mode children: parent's _children is the canonical owner.
|
|
2943
|
+
// Parent.unmount() cascades to _children for proper teardown.
|
|
2944
|
+
this._createLines.push(` (${s}._children || (${s}._children = [])).push(${instVar});`);
|
|
2945
|
+
}
|
|
2946
|
+
this._createLines.push(`}`);
|
|
2947
|
+
this._createLines.push(`} catch (__childErr) {`);
|
|
2948
|
+
this._createLines.push(` console.error('[Rip] ${componentName} construction failed:', __childErr);`);
|
|
2949
|
+
// If _init succeeded but _create threw, the partial instance has
|
|
2950
|
+
// _init-time effects, contexts, and possibly children registered.
|
|
2951
|
+
// Unmount it (with removeDOM:false; nothing was inserted into DOM
|
|
2952
|
+
// yet) so those resources release. unmount is idempotent so
|
|
2953
|
+
// redundant calls are safe.
|
|
2954
|
+
this._createLines.push(` if (${instVar}) { try { ${instVar}.unmount({removeDOM: false}); } catch (__ue) { console.error('[Rip] partial-child unmount error:', __ue); } }`);
|
|
2955
|
+
this._createLines.push(` ${instVar} = null;`);
|
|
2956
|
+
this._createLines.push(` ${elVar} = document.createComment('rip:child-error: ${componentName}');`);
|
|
2957
|
+
this._createLines.push(`}`);
|
|
2450
2958
|
this._createLines.push(`} finally { __popComponent(__prev); } }`);
|
|
2451
2959
|
|
|
2452
2960
|
for (const { event, value } of eventBindings) {
|
|
2453
2961
|
const handlerCode = this.emitInComponent(value, 'value');
|
|
2454
|
-
this._createLines.push(
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2962
|
+
this._createLines.push(`if (${instVar}) ${elVar}.addEventListener('${event}', (e) => __batch(() => (${handlerCode})(e)));`);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// Per-child push wrap for the full lifecycle invocation. ALL of
|
|
2966
|
+
// beforeMount, _setup, and mounted run with the child as the
|
|
2967
|
+
// current component, so any __effect they create auto-registers
|
|
2968
|
+
// on the child's _disposers (via the __getCurrentComponent
|
|
2969
|
+
// bridge), not the parent's. Guards:
|
|
2970
|
+
// - instVar null-check (failed-init placeholder branch above).
|
|
2971
|
+
// - _isSetup flag so the lifecycle runs ONCE even when this
|
|
2972
|
+
// setupLines block is re-executed by a factory's p() on every
|
|
2973
|
+
// reactive update. Class mode runs setupLines once anyway;
|
|
2974
|
+
// the flag is harmless there.
|
|
2975
|
+
// - Flag set BEFORE the calls so a recursive setup couldn't
|
|
2976
|
+
// re-enter and loop.
|
|
2977
|
+
// - beforeMount fires before _setup so user code has a hook
|
|
2978
|
+
// after construction but before reactive bindings activate
|
|
2979
|
+
// (matches the renderer's contract for page/layout
|
|
2980
|
+
// components).
|
|
2981
|
+
this._setupLines.push(`if (${instVar} && !${instVar}._isSetup) { ${instVar}._isSetup = true; const __cprev = __pushComponent(${instVar}); try { try { if (${instVar}.beforeMount) ${instVar}.beforeMount(); if (${instVar}._setup) ${instVar}._setup(); if (${instVar}.mounted) ${instVar}.mounted(); } catch (__e) { __handleComponentError(__e, ${instVar}); } } finally { __popComponent(__cprev); } }`);
|
|
2458
2982
|
|
|
2459
2983
|
for (const { key, valueCode } of reactiveProps) {
|
|
2460
|
-
|
|
2984
|
+
// Outer instVar guard: if _init / _create failed and the
|
|
2985
|
+
// placeholder branch substituted null for the instance, this
|
|
2986
|
+
// prop-updater effect would otherwise dereference null on every
|
|
2987
|
+
// signal change. The guard mirrors the event-listener emission
|
|
2988
|
+
// a few lines above.
|
|
2989
|
+
this._pushEffect(`if (${instVar}) { if (${instVar}.${key} && typeof ${instVar}.${key} === 'object' && 'value' in ${instVar}.${key}) ${instVar}.${key}.value = ${valueCode}; else if (${instVar}._setRestProp) ${instVar}._setRestProp('${key}', ${valueCode}); }`);
|
|
2461
2990
|
}
|
|
2462
2991
|
|
|
2463
2992
|
for (const line of childrenSetupLines) {
|
|
@@ -2512,9 +3041,24 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2512
3041
|
}
|
|
2513
3042
|
};
|
|
2514
3043
|
|
|
3044
|
+
// Bare identifier args become boolean prop shorthand:
|
|
3045
|
+
// Button outline, link, "Save"
|
|
3046
|
+
// is equivalent to:
|
|
3047
|
+
// Button outline: true, link: true, "Save"
|
|
3048
|
+
// Matches JSX semantics — the bare identifier is ALWAYS a literal `true`
|
|
3049
|
+
// prop key, never a variable reference, even if a same-named local
|
|
3050
|
+
// binding exists in scope. To pass a variable, write `outline: outline`.
|
|
3051
|
+
// Scoped to PascalCase component calls (this function's only caller is
|
|
3052
|
+
// emitChildComponent), so DOM element calls and non-render imperative
|
|
3053
|
+
// calls are unaffected.
|
|
3054
|
+
const BARE_IDENT_RE = /^[a-zA-Z_$][\w$]*$/;
|
|
3055
|
+
const isBareIdent = (a) => typeof a === 'string' && BARE_IDENT_RE.test(a);
|
|
3056
|
+
|
|
2515
3057
|
for (const arg of args) {
|
|
2516
3058
|
if (this.is(arg, 'object')) {
|
|
2517
3059
|
addObjectProps(arg);
|
|
3060
|
+
} else if (isBareIdent(arg)) {
|
|
3061
|
+
addProp(arg, 'true');
|
|
2518
3062
|
} else if (Array.isArray(arg) && (arg[0] === '->' || arg[0] === '=>')) {
|
|
2519
3063
|
let block = arg[2];
|
|
2520
3064
|
if (block) {
|
|
@@ -2531,6 +3075,10 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2531
3075
|
}
|
|
2532
3076
|
|
|
2533
3077
|
if (block) {
|
|
3078
|
+
// Save _createLines/_setupLines only — _factoryVars and
|
|
3079
|
+
// _renderLocalScope are intentionally shared because the
|
|
3080
|
+
// children-block JS is spliced back into the parent factory's
|
|
3081
|
+
// body (same JS function, same lexical scope).
|
|
2534
3082
|
const savedCreateLines = this._createLines;
|
|
2535
3083
|
const savedSetupLines = this._setupLines;
|
|
2536
3084
|
this._createLines = [];
|
|
@@ -2558,7 +3106,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2558
3106
|
this._createLines.push(`${textVar} = document.createTextNode('');`);
|
|
2559
3107
|
const body = `${textVar}.data = ${exprCode};`;
|
|
2560
3108
|
const effect = this._factoryMode
|
|
2561
|
-
? `disposers.push(__effect(() => { ${body} }));`
|
|
3109
|
+
? `disposers.push(__effect(() => { ${body} }, {skipRegister: true}));`
|
|
2562
3110
|
: `__effect(() => { ${body} });`;
|
|
2563
3111
|
childrenSetupLines.push(effect);
|
|
2564
3112
|
} else {
|
|
@@ -2598,6 +3146,16 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2598
3146
|
return true;
|
|
2599
3147
|
}
|
|
2600
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
|
+
|
|
2601
3159
|
// Method call on component: [['.', 'this', method], ...args]
|
|
2602
3160
|
// Methods may read reactive state internally — treat as reactive so the
|
|
2603
3161
|
// call gets wrapped in __effect and re-runs when dependencies change.
|
|
@@ -2613,6 +3171,32 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2613
3171
|
return false;
|
|
2614
3172
|
};
|
|
2615
3173
|
|
|
3174
|
+
// _collectBodyBindings — gather every name bound inside a render subtree
|
|
3175
|
+
// --------------------------------------------------------------------------
|
|
3176
|
+
// Used by emitTemplateLoop to skip auto-allocating an outer index name
|
|
3177
|
+
// that would later show up as either a positional parameter (explicit
|
|
3178
|
+
// `for-in`/`for-of`/`for-as` var at any depth) or a hoisted `let`
|
|
3179
|
+
// (`name = expr` at any depth) inside a descendant factory function. A
|
|
3180
|
+
// collision at any nesting depth is real because the outer index is
|
|
3181
|
+
// threaded as a positional parameter of EVERY descendant factory.
|
|
3182
|
+
|
|
3183
|
+
proto._collectBodyBindings = function(node, set) {
|
|
3184
|
+
if (!Array.isArray(node)) return;
|
|
3185
|
+
const head = node[0];
|
|
3186
|
+
if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
|
|
3187
|
+
const vars = node[1];
|
|
3188
|
+
const names = Array.isArray(vars) ? vars : [vars];
|
|
3189
|
+
for (const n of names) {
|
|
3190
|
+
if (typeof n === 'string') set.add(n);
|
|
3191
|
+
}
|
|
3192
|
+
} else if (head === '=' && _isPlainIdentifier(node[1])) {
|
|
3193
|
+
set.add(node[1]);
|
|
3194
|
+
}
|
|
3195
|
+
for (let i = 1; i < node.length; i++) {
|
|
3196
|
+
this._collectBodyBindings(node[i], set);
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
|
|
2616
3200
|
// isSimpleAssignable — check if value is a plain reactive member (assignable target)
|
|
2617
3201
|
// --------------------------------------------------------------------------
|
|
2618
3202
|
|
|
@@ -2653,6 +3237,26 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2653
3237
|
return this._rootsAtThis(sexpr[1]);
|
|
2654
3238
|
};
|
|
2655
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
|
+
|
|
2656
3260
|
// ==========================================================================
|
|
2657
3261
|
// Component Runtime
|
|
2658
3262
|
// ==========================================================================
|
|
@@ -2670,8 +3274,21 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
2670
3274
|
let __currentComponent = null;
|
|
2671
3275
|
|
|
2672
3276
|
function __pushComponent(component) {
|
|
2673
|
-
component
|
|
3277
|
+
// The component stack tracks the currently-active scope (so __effect
|
|
3278
|
+
// and friends can find it). Parent assignment happens ONCE — on the
|
|
3279
|
+
// first push that has a non-self predecessor. Later pushes (mount,
|
|
3280
|
+
// beforeMount, factory re-entry) preserve the existing chain.
|
|
3281
|
+
//
|
|
3282
|
+
// Without the "set once" guard, the renderer's careful threading of
|
|
3283
|
+
// outer-layout -> inner-layout -> page would survive construction
|
|
3284
|
+
// but get clobbered the moment any of those components got re-pushed
|
|
3285
|
+
// for its own lifecycle — a subsequent push with prev=null would
|
|
3286
|
+
// overwrite the construction-time parent. Cross-layout context
|
|
3287
|
+
// (offer in outer / accept in page) was silently broken.
|
|
2674
3288
|
const prev = __currentComponent;
|
|
3289
|
+
if (component && component._parent == null && prev && prev !== component) {
|
|
3290
|
+
component._parent = prev;
|
|
3291
|
+
}
|
|
2675
3292
|
__currentComponent = component;
|
|
2676
3293
|
return prev;
|
|
2677
3294
|
}
|
|
@@ -2680,6 +3297,13 @@ function __popComponent(prev) {
|
|
|
2680
3297
|
__currentComponent = prev;
|
|
2681
3298
|
}
|
|
2682
3299
|
|
|
3300
|
+
// Bridge for the reactive runtime in compiler.js. __effect calls this to
|
|
3301
|
+
// auto-register its disposer with the component currently being
|
|
3302
|
+
// constructed/mounted, so the disposer fires on component unmount.
|
|
3303
|
+
function __getCurrentComponent() {
|
|
3304
|
+
return __currentComponent;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
2683
3307
|
function setContext(key, value) {
|
|
2684
3308
|
if (!__currentComponent) throw new Error('setContext must be called during component initialization');
|
|
2685
3309
|
if (!__currentComponent._context) __currentComponent._context = new Map();
|
|
@@ -2688,7 +3312,11 @@ function setContext(key, value) {
|
|
|
2688
3312
|
|
|
2689
3313
|
function getContext(key) {
|
|
2690
3314
|
let component = __currentComponent;
|
|
2691
|
-
|
|
3315
|
+
// Cycle guard: see __handleComponentError above. A buggy _parent
|
|
3316
|
+
// chain shouldn't hang lookup of a missing key.
|
|
3317
|
+
const visited = new Set();
|
|
3318
|
+
while (component && !visited.has(component)) {
|
|
3319
|
+
visited.add(component);
|
|
2692
3320
|
if (component._context && component._context.has(key)) return component._context.get(key);
|
|
2693
3321
|
component = component._parent;
|
|
2694
3322
|
}
|
|
@@ -2697,7 +3325,9 @@ function getContext(key) {
|
|
|
2697
3325
|
|
|
2698
3326
|
function hasContext(key) {
|
|
2699
3327
|
let component = __currentComponent;
|
|
2700
|
-
|
|
3328
|
+
const visited = new Set();
|
|
3329
|
+
while (component && !visited.has(component)) {
|
|
3330
|
+
visited.add(component);
|
|
2701
3331
|
if (component._context && component._context.has(key)) return true;
|
|
2702
3332
|
component = component._parent;
|
|
2703
3333
|
}
|
|
@@ -2744,6 +3374,7 @@ function __reconcile(anchor, state, items, ctx, factory, keyFn, ...outer) {
|
|
|
2744
3374
|
if (!parent) return;
|
|
2745
3375
|
|
|
2746
3376
|
const oldKeys = state.keys;
|
|
3377
|
+
const oldItems = state.items || [];
|
|
2747
3378
|
const oldBlocks = state.blocks;
|
|
2748
3379
|
const oldLen = oldKeys.length;
|
|
2749
3380
|
const newLen = items.length;
|
|
@@ -2765,14 +3396,24 @@ function __reconcile(anchor, state, items, ctx, factory, keyFn, ...outer) {
|
|
|
2765
3396
|
parent.insertBefore(frag, anchor);
|
|
2766
3397
|
}
|
|
2767
3398
|
state.keys = hasKeyFn ? newKeys : items.slice();
|
|
3399
|
+
state.items = items.slice();
|
|
2768
3400
|
state.blocks = newBlocks;
|
|
2769
3401
|
return;
|
|
2770
3402
|
}
|
|
2771
3403
|
|
|
2772
|
-
// Phase 1: prefix scan — skip p()
|
|
3404
|
+
// Phase 1: prefix scan — skip p() ONLY when key AND item identity
|
|
3405
|
+
// match. With a custom keyFn, a stable key can be reused across
|
|
3406
|
+
// different item references (e.g. when the user replaces an item
|
|
3407
|
+
// object with a new one that has the same id but different fields);
|
|
3408
|
+
// skipping p() in that case would leave the block displaying stale
|
|
3409
|
+
// data. Reference identity guards this.
|
|
2773
3410
|
let start = 0;
|
|
2774
3411
|
const minLen = oldLen < newLen ? oldLen : newLen;
|
|
2775
3412
|
while (start < minLen && oldKeys[start] === newKeys[start]) {
|
|
3413
|
+
if (oldItems[start] !== items[start]) {
|
|
3414
|
+
const block = oldBlocks[start];
|
|
3415
|
+
if (!block._s) block.p(ctx, items[start], start, ...outer);
|
|
3416
|
+
}
|
|
2776
3417
|
newBlocks[start] = oldBlocks[start];
|
|
2777
3418
|
start++;
|
|
2778
3419
|
}
|
|
@@ -2842,6 +3483,7 @@ function __reconcile(anchor, state, items, ctx, factory, keyFn, ...outer) {
|
|
|
2842
3483
|
}
|
|
2843
3484
|
|
|
2844
3485
|
state.keys = hasKeyFn ? newKeys : items.slice();
|
|
3486
|
+
state.items = items.slice();
|
|
2845
3487
|
state.blocks = newBlocks;
|
|
2846
3488
|
}
|
|
2847
3489
|
|
|
@@ -2886,7 +3528,13 @@ function __transition(el, name, dir, done) {
|
|
|
2886
3528
|
|
|
2887
3529
|
function __handleComponentError(error, component) {
|
|
2888
3530
|
let current = component;
|
|
2889
|
-
|
|
3531
|
+
// Defensive cycle guard: if the parent chain is corrupted (e.g. by
|
|
3532
|
+
// a buggy _parent assignment), we still terminate. The fix is in
|
|
3533
|
+
// __pushComponent above, but cycle detection here is cheap belt-
|
|
3534
|
+
// and-suspenders so a bad _parent never hangs the runtime.
|
|
3535
|
+
const visited = new Set();
|
|
3536
|
+
while (current && !visited.has(current)) {
|
|
3537
|
+
visited.add(current);
|
|
2890
3538
|
if (current.onError) {
|
|
2891
3539
|
try { current.onError(error, component); return; } catch (_) {}
|
|
2892
3540
|
}
|
|
@@ -2900,13 +3548,36 @@ class __Component {
|
|
|
2900
3548
|
Object.assign(this, props);
|
|
2901
3549
|
if (!this.app && globalThis.__ripApp) this.app = globalThis.__ripApp;
|
|
2902
3550
|
const prev = __pushComponent(this);
|
|
2903
|
-
try {
|
|
3551
|
+
try {
|
|
3552
|
+
this._init(props);
|
|
3553
|
+
} catch (e) {
|
|
3554
|
+
__popComponent(prev);
|
|
3555
|
+
// Mark this instance as having failed initialization so parent
|
|
3556
|
+
// emit-sites (emitChildComponent) can substitute a placeholder
|
|
3557
|
+
// instead of running _create / _setup on a broken instance.
|
|
3558
|
+
this._initFailed = true;
|
|
3559
|
+
// Run the user's error hook (parent-onError walk). If a boundary
|
|
3560
|
+
// handles it, control returns here and we leave the broken
|
|
3561
|
+
// instance to be handled via _initFailed. If no boundary exists,
|
|
3562
|
+
// __handleComponentError re-throws and the caller's outer
|
|
3563
|
+
// try/catch in emitChildComponent will substitute the same
|
|
3564
|
+
// placeholder.
|
|
3565
|
+
__handleComponentError(e, this);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
2904
3568
|
__popComponent(prev);
|
|
2905
3569
|
}
|
|
2906
3570
|
_init() {}
|
|
2907
3571
|
mount(target) {
|
|
2908
3572
|
if (typeof target === "string") target = document.querySelector(target);
|
|
2909
3573
|
this._target = target;
|
|
3574
|
+
// _create / _setup are wrapped with __pushComponent so any __effect
|
|
3575
|
+
// created inside (reactive attribute bindings, reactive text nodes,
|
|
3576
|
+
// child components, user '~>' effects) auto-registers its disposer
|
|
3577
|
+
// with this component via __getCurrentComponent in the runtime.
|
|
3578
|
+
// Without this wrapping, effects created here lived forever and
|
|
3579
|
+
// their cleanup functions never fired on unmount.
|
|
3580
|
+
const prev = __pushComponent(this);
|
|
2910
3581
|
try {
|
|
2911
3582
|
this._root = this._create();
|
|
2912
3583
|
if (this._root) target.appendChild(this._root);
|
|
@@ -2914,17 +3585,52 @@ class __Component {
|
|
|
2914
3585
|
if (this.mounted) this.mounted();
|
|
2915
3586
|
} catch (error) {
|
|
2916
3587
|
__handleComponentError(error, this);
|
|
3588
|
+
} finally {
|
|
3589
|
+
__popComponent(prev);
|
|
2917
3590
|
}
|
|
2918
3591
|
return this;
|
|
2919
3592
|
}
|
|
2920
|
-
unmount() {
|
|
3593
|
+
unmount({ removeDOM = true } = {}) {
|
|
3594
|
+
// Symmetric to mount: tear down lifecycle hooks, all auto-registered
|
|
3595
|
+
// effect disposers, child components, and (optionally) the DOM.
|
|
3596
|
+
//
|
|
3597
|
+
// beforeUnmount - user hook; runs while signals/effects are still
|
|
3598
|
+
// live, so user code can read final state.
|
|
3599
|
+
// children - cascade BEFORE this instance's disposers so
|
|
3600
|
+
// children can react to parent state during their
|
|
3601
|
+
// own teardown.
|
|
3602
|
+
// _disposers - effect disposers auto-registered by __effect
|
|
3603
|
+
// (cleared eagerly so a re-mount starts fresh).
|
|
3604
|
+
// unmounted - user hook; final notification.
|
|
3605
|
+
// DOM removal - skipped when caller wants to keep the old DOM
|
|
3606
|
+
// visible until replacement (route transitions).
|
|
3607
|
+
//
|
|
3608
|
+
// Idempotent: a child can be unmounted by its enclosing factory's
|
|
3609
|
+
// d(detaching) AND later by the parent's unmount cascade. Without
|
|
3610
|
+
// the _unmounted guard, beforeUnmount/unmounted hooks would re-fire
|
|
3611
|
+
// and cleanup would walk an already-empty graph for no benefit.
|
|
3612
|
+
if (this._unmounted) return;
|
|
3613
|
+
this._unmounted = true;
|
|
3614
|
+
try {
|
|
3615
|
+
if (this.beforeUnmount) this.beforeUnmount();
|
|
3616
|
+
} catch (e) { console.error('[Rip] beforeUnmount error:', e); }
|
|
2921
3617
|
if (this._children) {
|
|
2922
3618
|
for (const child of this._children) {
|
|
2923
|
-
child.unmount();
|
|
3619
|
+
try { child.unmount({ removeDOM }); }
|
|
3620
|
+
catch (e) { console.error('[Rip] child unmount error:', e); }
|
|
2924
3621
|
}
|
|
3622
|
+
this._children = null;
|
|
2925
3623
|
}
|
|
2926
|
-
if (this.
|
|
2927
|
-
|
|
3624
|
+
if (this._disposers) {
|
|
3625
|
+
for (const d of this._disposers) {
|
|
3626
|
+
try { d(); } catch (e) { console.error('[Rip] effect disposer error:', e); }
|
|
3627
|
+
}
|
|
3628
|
+
this._disposers = null;
|
|
3629
|
+
}
|
|
3630
|
+
try {
|
|
3631
|
+
if (this.unmounted) this.unmounted();
|
|
3632
|
+
} catch (e) { console.error('[Rip] unmounted error:', e); }
|
|
3633
|
+
if (removeDOM && this._root && this._root.parentNode) {
|
|
2928
3634
|
this._root.parentNode.removeChild(this._root);
|
|
2929
3635
|
}
|
|
2930
3636
|
}
|
|
@@ -2940,7 +3646,7 @@ class __Component {
|
|
|
2940
3646
|
|
|
2941
3647
|
// Register on globalThis for runtime deduplication
|
|
2942
3648
|
if (typeof globalThis !== 'undefined') {
|
|
2943
|
-
globalThis.__ripComponent = { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __Component };
|
|
3649
|
+
globalThis.__ripComponent = { __pushComponent, __popComponent, __getCurrentComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __Component };
|
|
2944
3650
|
}
|
|
2945
3651
|
|
|
2946
3652
|
`;
|