Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions dist/@rip/rip.js
Original file line number Diff line number Diff line change
Expand Up @@ -17830,12 +17830,37 @@ ${this.replayPad}}` : " }");
line(() => this.b.emit(`{ const ${kidV} = ${instVar}._beginProjection(${self()}); try {`));
const outerHost = this.projectionHost;
this.projectionHost = instVar;
const from = rec.setups.length;
let childrenVar;
try {
childrenVar = projection();
} finally {
this.projectionHost = outerHost;
}
const guard = (setups) => ({
kind: "raw",
node: null,
fn: (pad) => {
this.b.emit(`${pad}if (${instVar}) {
`);
this.replaySetups({ setups }, `${pad} `);
this.b.emit(`${pad}}
`);
}
});
let run = [];
for (const s of rec.setups.splice(from)) {
if (s.latch !== true) {
run.push(s);
continue;
}
if (run.length > 0)
rec.setups.push(guard(run));
run = [];
rec.setups.push(s);
}
if (run.length > 0)
rec.setups.push(guard(run));
line(() => this.b.emit(`} finally { ${instVar}._endProjection(${kidV}); } }`));
line(() => this.b.emit(`${instVar}._setChildren(${childrenVar});`));
}
Expand Down Expand Up @@ -17907,6 +17932,7 @@ ${this.replayPad}}` : " }");
}
rec.setups.push({
kind: "raw",
latch: true,
fn: (pad) => {
this.b.emit(pad);
const emitLatch = () => {
Expand Down Expand Up @@ -26271,6 +26297,9 @@ function __lis(arr) {
return result;
}
function __reconcile(anchor, state, items, ctx, factory, keyFn, ...outer) {
if (anchor == null) {
throw new Error("__reconcile: no anchor — the list's create phase never placed one");
}
const parent = anchor.parentNode;
if (!parent)
return;
Expand Down
100 changes: 51 additions & 49 deletions dist/@rip/rip.min.js

Large diffs are not rendered by default.

Binary file modified dist/@rip/rip.min.js.br
Binary file not shown.
4 changes: 0 additions & 4 deletions packages/tui/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,3 @@ steps are in [PLAN.md](PLAN.md).
- A spelling for a capture listener in a render block. The package reads
a type that ends in `Capture` (`@keydownCapture:`), since `@name:` is
always `addEventListener(name, handler)` with no third argument.
- A name that is not defined, read as a prop of an element that holds a
keyed `for`, surfaces as a reconciler `TypeError` (`anchor.parentNode`,
`src/runtime/components.js:590`) with no node named, where the same
read on an element without the `for` surfaces as the `ReferenceError`.
17 changes: 17 additions & 0 deletions packages/tui/test.rip
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,23 @@ test "a child that fails to construct fails the mount, through the runtime's rep
console.error = held
eq printed, []

test "a child that fails to construct while holding a keyed for fails the mount with its own error", ->
Listed = component
@items := ['a']
render
Box flexDirection: 'column'
Box contentOffsetY: nosuchname
for item, n in @items
Text key: n, "#{item}"
held = console.error
printed = []
console.error = (label) -> printed.push label
try
throws (-> renderToString Listed), ReferenceError, 'nosuchname is not defined'
finally
console.error = held
eq printed, []

test "restore hands the runtime its own reporter back", ->
renderToString (component
render
Expand Down
26 changes: 26 additions & 0 deletions src/emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -12104,8 +12104,33 @@ class Emitter {
line(() => this.b.emit(`{ const ${kidV} = ${instVar}._beginProjection(${self()}); try {`));
const outerHost = this.projectionHost;
this.projectionHost = instVar;
const from = rec.setups.length;
let childrenVar;
try { childrenVar = projection(); } finally { this.projectionHost = outerHost; }
// The projection's setups — its lists, its branches, its text
// bindings — run only while the child stands: a child whose
// construction failed is null at its construct site, and the
// nodes and anchors those setups drive may never have been made.
// Their error would mask the child's own. A projected child's
// latch is the exception, in its place: it guards itself, and a
// child the projection made must leave `mounting` to unmount.
const guard = (setups) => ({
kind: 'raw',
node: null,
fn: (pad) => {
this.b.emit(`${pad}if (${instVar}) {\n`);
this.replaySetups({ setups }, `${pad} `);
this.b.emit(`${pad}}\n`);
},
});
let run = [];
for (const s of rec.setups.splice(from)) {
if (s.latch !== true) { run.push(s); continue; }
if (run.length > 0) rec.setups.push(guard(run));
run = [];
rec.setups.push(s);
}
if (run.length > 0) rec.setups.push(guard(run));
line(() => this.b.emit(`} finally { ${instVar}._endProjection(${kidV}); } }`));
line(() => this.b.emit(`${instVar}._setChildren(${childrenVar});`));
}
Expand Down Expand Up @@ -12191,6 +12216,7 @@ class Emitter {
// the placeholder rather than the detached failed root. ──
rec.setups.push({
kind: 'raw',
latch: true,
fn: (pad) => {
this.b.emit(pad);
const emitLatch = () => {
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,12 @@ function __lis(arr) {
}

function __reconcile(anchor, state, items, ctx, factory, keyFn, ...outer) {
// The anchor is the comment the list's create phase placed; a list
// whose create phase never ran has none, and reconciling against
// nothing is a contract violation, not a list with no parent.
if (anchor == null) {
throw new Error('__reconcile: no anchor — the list\'s create phase never placed one');
}
const parent = anchor.parentNode;
if (!parent) return;

Expand Down
97 changes: 97 additions & 0 deletions test/ui/components.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,103 @@ describe('child components: the instantiation protocol', () => {
}
});

test('a child whose construction throws while holding projected blocks is contained at its construct site: the report is its own error, and the mount stands', () => {
const source = `Kid = component
render
b "kid"
Wrap = component
render
div
slot
Broken = component
render
div title: missing
slot
App = component
items := ['a']
show := true
name := 'n'
render
div
Wrap title: nosuchname
for item, i in items
span key: i, "#{item}"
if show
em "shown"
span "#{name}"
Broken
Kid
for item, i in items
span key: i, "#{item}"
strong "kept"
`;
const { code } = compile(source, { runtimeDelivery: 'none' });
const names = Object.keys(RT);
const { App } = new Function(...names, `${code}\nreturn { App };`)(...names.map((name) => RT[name]));
const target = document.createElement('main');
const app = new App({});
const prior = console.error;
const reported = [];
console.error = (label, error) => reported.push([label, error]);
try {
app.mount(target);
const stood = '<main><div data-part="App"><!--rip:child-error: Wrap--><!--rip:child-error: Broken--><strong>kept</strong></div></main>';
expect(serialize(target)).toBe(stood);
app.items.value = ['a', 'b'];
app.show.value = false;
app.name.value = 'm';
expect(serialize(target)).toBe(stood);
app.unmount();
} finally {
console.error = prior;
}
expect(reported.map(([label, error]) => [label, error?.constructor, error?.message])).toEqual([
['[Rip] Wrap construction failed:', ReferenceError, 'nosuchname is not defined'],
['[Rip] Broken construction failed:', ReferenceError, 'missing is not defined'],
]);
});

test('a child whose _init a boundary contained holds no projected blocks: the mount stands on the init placeholder', () => {
const source = `caught = []
Bad = component
x := nosuchname
render
div
slot
App = component
items := ['a']
onError = (failure) -> caught.push failure
render
div
Bad
for item, i in items
span key: i, "#{item}"
`;
const { code } = compile(source, { runtimeDelivery: 'none' });
const names = Object.keys(RT);
const { App, caught } = new Function(...names, `${code}\nreturn { App, caught };`)(...names.map((name) => RT[name]));
const target = document.createElement('main');
const app = new App({});
const prior = console.error;
const printed = [];
console.error = (...args) => printed.push(args);
try {
app.mount(target);
} finally {
console.error = prior;
}
try {
expect(printed).toEqual([]);
expect(caught).toHaveLength(1);
expect(caught[0].error).toBeInstanceOf(ReferenceError);
expect(serialize(target)).toBe('<main><div data-part="App"><!--rip:child-init-failed: Bad--></div></main>');
app.items.value = ['a', 'b'];
expect(serialize(target)).toBe('<main><div data-part="App"><!--rip:child-init-failed: Bad--></div></main>');
} finally {
app.unmount();
}
});

test('failed factory children transfer ownership to their placeholders across keyed moves, removal, and branch replacement', () => {
const source = `failedRoots = []
Bad = component
Expand Down
Loading