Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error on duplicate partial name #1958

Merged
merged 1 commit into from
Oct 18, 2023
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
9 changes: 8 additions & 1 deletion src/server/rendering/preact_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,21 @@ options.__b = (vnode: VNode<Record<string, unknown>>) => {
`<Partial> components cannot be used inside islands.`,
);
}
const name = vnode.props.name as string;
if (current.encounteredPartials.has(name)) {
current.error = new Error(
`Duplicate partial name "${name}" found. The partial name prop is expected to be unique among partial components.`,
);
}
current.encounteredPartials.add(name);

const mode = encodePartialMode(
// deno-lint-ignore no-explicit-any
(vnode.props as any).mode ?? "replace",
);
vnode.props.children = wrapWithMarker(
vnode.props.children,
`frsh-partial:${vnode.props.name}:${mode}:${vnode.key ?? ""}`,
`frsh-partial:${name}:${mode}:${vnode.key ?? ""}`,
);
} else if (
vnode.key && (current.islandDepth > 0 || current.partialDepth > 0)
Expand Down
2 changes: 2 additions & 0 deletions src/server/rendering/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class RenderState {
#nonce = "";
error: Error | null = null;
isPartial: boolean;
encounteredPartials = new Set<string>();
partialCount = 0;
partialDepth = 0;
islandDepth = 0;
Expand Down Expand Up @@ -71,5 +72,6 @@ export class RenderState {
this.renderingUserTemplate = false;
this.ownerStack = [];
this.owners.clear();
this.encounteredPartials.clear();
}
}
282 changes: 142 additions & 140 deletions tests/fixture_partials/fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,76 +34,77 @@ import * as $28 from "./routes/client_nav_opt_out/page-c.tsx";
import * as $29 from "./routes/deep_partial/index.tsx";
import * as $30 from "./routes/deep_partial/injected.tsx";
import * as $31 from "./routes/deep_partial/update.tsx";
import * as $32 from "./routes/form/index.tsx";
import * as $33 from "./routes/form/injected.tsx";
import * as $34 from "./routes/form/update.tsx";
import * as $35 from "./routes/form_get/index.tsx";
import * as $36 from "./routes/form_post/index.tsx";
import * as $37 from "./routes/form_submitter/index.tsx";
import * as $38 from "./routes/form_submitter_partial/index.tsx";
import * as $39 from "./routes/form_submitter_partial_no_client_nav/index.tsx";
import * as $40 from "./routes/fragment_nav.tsx";
import * as $41 from "./routes/fragment_nav_scroll.tsx";
import * as $42 from "./routes/head_merge/duplicate.tsx";
import * as $43 from "./routes/head_merge/index.tsx";
import * as $44 from "./routes/head_merge/injected.tsx";
import * as $45 from "./routes/head_merge/update.tsx";
import * as $46 from "./routes/index.tsx";
import * as $47 from "./routes/island_instance/index.tsx";
import * as $48 from "./routes/island_instance/injected.tsx";
import * as $49 from "./routes/island_instance/partial.tsx";
import * as $50 from "./routes/island_instance/partial_remove.tsx";
import * as $51 from "./routes/island_instance/partial_replace.tsx";
import * as $52 from "./routes/island_instance_multiple/index.tsx";
import * as $53 from "./routes/island_instance_multiple/injected.tsx";
import * as $54 from "./routes/island_instance_multiple/partial.tsx";
import * as $55 from "./routes/island_instance_multiple/partial_both.tsx";
import * as $56 from "./routes/island_instance_nested/index.tsx";
import * as $57 from "./routes/island_instance_nested/injected.tsx";
import * as $58 from "./routes/island_instance_nested/partial.tsx";
import * as $59 from "./routes/island_instance_nested/replace.tsx";
import * as $60 from "./routes/island_props/index.tsx";
import * as $61 from "./routes/island_props/injected.tsx";
import * as $62 from "./routes/island_props/partial.tsx";
import * as $63 from "./routes/island_props_signals/index.tsx";
import * as $64 from "./routes/island_props_signals/injected.tsx";
import * as $65 from "./routes/island_props_signals/partial.tsx";
import * as $66 from "./routes/keys/index.tsx";
import * as $67 from "./routes/keys/injected.tsx";
import * as $68 from "./routes/keys/swap.tsx";
import * as $69 from "./routes/keys_components/index.tsx";
import * as $70 from "./routes/keys_components/injected.tsx";
import * as $71 from "./routes/keys_components/swap.tsx";
import * as $72 from "./routes/keys_confusion/index.tsx";
import * as $73 from "./routes/keys_dom/index.tsx";
import * as $74 from "./routes/keys_dom/injected.tsx";
import * as $75 from "./routes/keys_dom/swap.tsx";
import * as $76 from "./routes/keys_outside/index.tsx";
import * as $77 from "./routes/loading/index.tsx";
import * as $78 from "./routes/loading/injected.tsx";
import * as $79 from "./routes/loading/update.tsx";
import * as $80 from "./routes/missing_partial/index.tsx";
import * as $81 from "./routes/missing_partial/injected.tsx";
import * as $82 from "./routes/missing_partial/update.tsx";
import * as $83 from "./routes/mode/append.tsx";
import * as $84 from "./routes/mode/index.tsx";
import * as $85 from "./routes/mode/injected.tsx";
import * as $86 from "./routes/mode/prepend.tsx";
import * as $87 from "./routes/mode/replace.tsx";
import * as $88 from "./routes/nested/index.tsx";
import * as $89 from "./routes/nested/inner.tsx";
import * as $90 from "./routes/nested/outer.tsx";
import * as $91 from "./routes/no_islands/index.tsx";
import * as $92 from "./routes/no_islands/injected.tsx";
import * as $93 from "./routes/no_islands/update.tsx";
import * as $94 from "./routes/no_partial_response/index.tsx";
import * as $95 from "./routes/no_partial_response/injected.tsx";
import * as $96 from "./routes/no_partial_response/update.tsx";
import * as $97 from "./routes/partial_slot_inside_island.tsx";
import * as $98 from "./routes/relative_link/index.tsx";
import * as $99 from "./routes/scroll_restoration/index.tsx";
import * as $100 from "./routes/scroll_restoration/injected.tsx";
import * as $101 from "./routes/scroll_restoration/update.tsx";
import * as $32 from "./routes/duplicate_name/index.tsx";
import * as $33 from "./routes/form/index.tsx";
import * as $34 from "./routes/form/injected.tsx";
import * as $35 from "./routes/form/update.tsx";
import * as $36 from "./routes/form_get/index.tsx";
import * as $37 from "./routes/form_post/index.tsx";
import * as $38 from "./routes/form_submitter/index.tsx";
import * as $39 from "./routes/form_submitter_partial/index.tsx";
import * as $40 from "./routes/form_submitter_partial_no_client_nav/index.tsx";
import * as $41 from "./routes/fragment_nav.tsx";
import * as $42 from "./routes/fragment_nav_scroll.tsx";
import * as $43 from "./routes/head_merge/duplicate.tsx";
import * as $44 from "./routes/head_merge/index.tsx";
import * as $45 from "./routes/head_merge/injected.tsx";
import * as $46 from "./routes/head_merge/update.tsx";
import * as $47 from "./routes/index.tsx";
import * as $48 from "./routes/island_instance/index.tsx";
import * as $49 from "./routes/island_instance/injected.tsx";
import * as $50 from "./routes/island_instance/partial.tsx";
import * as $51 from "./routes/island_instance/partial_remove.tsx";
import * as $52 from "./routes/island_instance/partial_replace.tsx";
import * as $53 from "./routes/island_instance_multiple/index.tsx";
import * as $54 from "./routes/island_instance_multiple/injected.tsx";
import * as $55 from "./routes/island_instance_multiple/partial.tsx";
import * as $56 from "./routes/island_instance_multiple/partial_both.tsx";
import * as $57 from "./routes/island_instance_nested/index.tsx";
import * as $58 from "./routes/island_instance_nested/injected.tsx";
import * as $59 from "./routes/island_instance_nested/partial.tsx";
import * as $60 from "./routes/island_instance_nested/replace.tsx";
import * as $61 from "./routes/island_props/index.tsx";
import * as $62 from "./routes/island_props/injected.tsx";
import * as $63 from "./routes/island_props/partial.tsx";
import * as $64 from "./routes/island_props_signals/index.tsx";
import * as $65 from "./routes/island_props_signals/injected.tsx";
import * as $66 from "./routes/island_props_signals/partial.tsx";
import * as $67 from "./routes/keys/index.tsx";
import * as $68 from "./routes/keys/injected.tsx";
import * as $69 from "./routes/keys/swap.tsx";
import * as $70 from "./routes/keys_components/index.tsx";
import * as $71 from "./routes/keys_components/injected.tsx";
import * as $72 from "./routes/keys_components/swap.tsx";
import * as $73 from "./routes/keys_confusion/index.tsx";
import * as $74 from "./routes/keys_dom/index.tsx";
import * as $75 from "./routes/keys_dom/injected.tsx";
import * as $76 from "./routes/keys_dom/swap.tsx";
import * as $77 from "./routes/keys_outside/index.tsx";
import * as $78 from "./routes/loading/index.tsx";
import * as $79 from "./routes/loading/injected.tsx";
import * as $80 from "./routes/loading/update.tsx";
import * as $81 from "./routes/missing_partial/index.tsx";
import * as $82 from "./routes/missing_partial/injected.tsx";
import * as $83 from "./routes/missing_partial/update.tsx";
import * as $84 from "./routes/mode/append.tsx";
import * as $85 from "./routes/mode/index.tsx";
import * as $86 from "./routes/mode/injected.tsx";
import * as $87 from "./routes/mode/prepend.tsx";
import * as $88 from "./routes/mode/replace.tsx";
import * as $89 from "./routes/nested/index.tsx";
import * as $90 from "./routes/nested/inner.tsx";
import * as $91 from "./routes/nested/outer.tsx";
import * as $92 from "./routes/no_islands/index.tsx";
import * as $93 from "./routes/no_islands/injected.tsx";
import * as $94 from "./routes/no_islands/update.tsx";
import * as $95 from "./routes/no_partial_response/index.tsx";
import * as $96 from "./routes/no_partial_response/injected.tsx";
import * as $97 from "./routes/no_partial_response/update.tsx";
import * as $98 from "./routes/partial_slot_inside_island.tsx";
import * as $99 from "./routes/relative_link/index.tsx";
import * as $100 from "./routes/scroll_restoration/index.tsx";
import * as $101 from "./routes/scroll_restoration/injected.tsx";
import * as $102 from "./routes/scroll_restoration/update.tsx";
import * as $$0 from "./islands/Counter.tsx";
import * as $$1 from "./islands/CounterA.tsx";
import * as $$2 from "./islands/CounterB.tsx";
Expand Down Expand Up @@ -154,76 +155,77 @@ const manifest = {
"./routes/deep_partial/index.tsx": $29,
"./routes/deep_partial/injected.tsx": $30,
"./routes/deep_partial/update.tsx": $31,
"./routes/form/index.tsx": $32,
"./routes/form/injected.tsx": $33,
"./routes/form/update.tsx": $34,
"./routes/form_get/index.tsx": $35,
"./routes/form_post/index.tsx": $36,
"./routes/form_submitter/index.tsx": $37,
"./routes/form_submitter_partial/index.tsx": $38,
"./routes/form_submitter_partial_no_client_nav/index.tsx": $39,
"./routes/fragment_nav.tsx": $40,
"./routes/fragment_nav_scroll.tsx": $41,
"./routes/head_merge/duplicate.tsx": $42,
"./routes/head_merge/index.tsx": $43,
"./routes/head_merge/injected.tsx": $44,
"./routes/head_merge/update.tsx": $45,
"./routes/index.tsx": $46,
"./routes/island_instance/index.tsx": $47,
"./routes/island_instance/injected.tsx": $48,
"./routes/island_instance/partial.tsx": $49,
"./routes/island_instance/partial_remove.tsx": $50,
"./routes/island_instance/partial_replace.tsx": $51,
"./routes/island_instance_multiple/index.tsx": $52,
"./routes/island_instance_multiple/injected.tsx": $53,
"./routes/island_instance_multiple/partial.tsx": $54,
"./routes/island_instance_multiple/partial_both.tsx": $55,
"./routes/island_instance_nested/index.tsx": $56,
"./routes/island_instance_nested/injected.tsx": $57,
"./routes/island_instance_nested/partial.tsx": $58,
"./routes/island_instance_nested/replace.tsx": $59,
"./routes/island_props/index.tsx": $60,
"./routes/island_props/injected.tsx": $61,
"./routes/island_props/partial.tsx": $62,
"./routes/island_props_signals/index.tsx": $63,
"./routes/island_props_signals/injected.tsx": $64,
"./routes/island_props_signals/partial.tsx": $65,
"./routes/keys/index.tsx": $66,
"./routes/keys/injected.tsx": $67,
"./routes/keys/swap.tsx": $68,
"./routes/keys_components/index.tsx": $69,
"./routes/keys_components/injected.tsx": $70,
"./routes/keys_components/swap.tsx": $71,
"./routes/keys_confusion/index.tsx": $72,
"./routes/keys_dom/index.tsx": $73,
"./routes/keys_dom/injected.tsx": $74,
"./routes/keys_dom/swap.tsx": $75,
"./routes/keys_outside/index.tsx": $76,
"./routes/loading/index.tsx": $77,
"./routes/loading/injected.tsx": $78,
"./routes/loading/update.tsx": $79,
"./routes/missing_partial/index.tsx": $80,
"./routes/missing_partial/injected.tsx": $81,
"./routes/missing_partial/update.tsx": $82,
"./routes/mode/append.tsx": $83,
"./routes/mode/index.tsx": $84,
"./routes/mode/injected.tsx": $85,
"./routes/mode/prepend.tsx": $86,
"./routes/mode/replace.tsx": $87,
"./routes/nested/index.tsx": $88,
"./routes/nested/inner.tsx": $89,
"./routes/nested/outer.tsx": $90,
"./routes/no_islands/index.tsx": $91,
"./routes/no_islands/injected.tsx": $92,
"./routes/no_islands/update.tsx": $93,
"./routes/no_partial_response/index.tsx": $94,
"./routes/no_partial_response/injected.tsx": $95,
"./routes/no_partial_response/update.tsx": $96,
"./routes/partial_slot_inside_island.tsx": $97,
"./routes/relative_link/index.tsx": $98,
"./routes/scroll_restoration/index.tsx": $99,
"./routes/scroll_restoration/injected.tsx": $100,
"./routes/scroll_restoration/update.tsx": $101,
"./routes/duplicate_name/index.tsx": $32,
"./routes/form/index.tsx": $33,
"./routes/form/injected.tsx": $34,
"./routes/form/update.tsx": $35,
"./routes/form_get/index.tsx": $36,
"./routes/form_post/index.tsx": $37,
"./routes/form_submitter/index.tsx": $38,
"./routes/form_submitter_partial/index.tsx": $39,
"./routes/form_submitter_partial_no_client_nav/index.tsx": $40,
"./routes/fragment_nav.tsx": $41,
"./routes/fragment_nav_scroll.tsx": $42,
"./routes/head_merge/duplicate.tsx": $43,
"./routes/head_merge/index.tsx": $44,
"./routes/head_merge/injected.tsx": $45,
"./routes/head_merge/update.tsx": $46,
"./routes/index.tsx": $47,
"./routes/island_instance/index.tsx": $48,
"./routes/island_instance/injected.tsx": $49,
"./routes/island_instance/partial.tsx": $50,
"./routes/island_instance/partial_remove.tsx": $51,
"./routes/island_instance/partial_replace.tsx": $52,
"./routes/island_instance_multiple/index.tsx": $53,
"./routes/island_instance_multiple/injected.tsx": $54,
"./routes/island_instance_multiple/partial.tsx": $55,
"./routes/island_instance_multiple/partial_both.tsx": $56,
"./routes/island_instance_nested/index.tsx": $57,
"./routes/island_instance_nested/injected.tsx": $58,
"./routes/island_instance_nested/partial.tsx": $59,
"./routes/island_instance_nested/replace.tsx": $60,
"./routes/island_props/index.tsx": $61,
"./routes/island_props/injected.tsx": $62,
"./routes/island_props/partial.tsx": $63,
"./routes/island_props_signals/index.tsx": $64,
"./routes/island_props_signals/injected.tsx": $65,
"./routes/island_props_signals/partial.tsx": $66,
"./routes/keys/index.tsx": $67,
"./routes/keys/injected.tsx": $68,
"./routes/keys/swap.tsx": $69,
"./routes/keys_components/index.tsx": $70,
"./routes/keys_components/injected.tsx": $71,
"./routes/keys_components/swap.tsx": $72,
"./routes/keys_confusion/index.tsx": $73,
"./routes/keys_dom/index.tsx": $74,
"./routes/keys_dom/injected.tsx": $75,
"./routes/keys_dom/swap.tsx": $76,
"./routes/keys_outside/index.tsx": $77,
"./routes/loading/index.tsx": $78,
"./routes/loading/injected.tsx": $79,
"./routes/loading/update.tsx": $80,
"./routes/missing_partial/index.tsx": $81,
"./routes/missing_partial/injected.tsx": $82,
"./routes/missing_partial/update.tsx": $83,
"./routes/mode/append.tsx": $84,
"./routes/mode/index.tsx": $85,
"./routes/mode/injected.tsx": $86,
"./routes/mode/prepend.tsx": $87,
"./routes/mode/replace.tsx": $88,
"./routes/nested/index.tsx": $89,
"./routes/nested/inner.tsx": $90,
"./routes/nested/outer.tsx": $91,
"./routes/no_islands/index.tsx": $92,
"./routes/no_islands/injected.tsx": $93,
"./routes/no_islands/update.tsx": $94,
"./routes/no_partial_response/index.tsx": $95,
"./routes/no_partial_response/injected.tsx": $96,
"./routes/no_partial_response/update.tsx": $97,
"./routes/partial_slot_inside_island.tsx": $98,
"./routes/relative_link/index.tsx": $99,
"./routes/scroll_restoration/index.tsx": $100,
"./routes/scroll_restoration/injected.tsx": $101,
"./routes/scroll_restoration/update.tsx": $102,
},
islands: {
"./islands/Counter.tsx": $$0,
Expand Down
29 changes: 29 additions & 0 deletions tests/fixture_partials/routes/duplicate_name/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Partial } from "$fresh/runtime.ts";
import { PageProps } from "$fresh/server.ts";

export default function SlotDemo(props: PageProps) {
const update = props.url.searchParams.has("swap");

return (
<div>
<Partial name="slot-1">
<h2>foo</h2>
<p>some text</p>
</Partial>
{update && (
<Partial name="slot-1">
<p>foo</p>
</Partial>
)}
<p>
<a
class="swap-link"
href="/duplicate_name"
f-partial="/duplicate_name?swap=foo"
>
swap
</a>
</p>
</div>
);
}
18 changes: 18 additions & 0 deletions tests/partials_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1405,3 +1405,21 @@ Deno.test("nested partials are able to be updated", async () => {
},
);
});

Deno.test("errors on duplicate partial name", async () => {
await withPageName(
"./tests/fixture_partials/main.ts",
async (page, address) => {
await page.goto(`${address}/duplicate_name`);
await page.waitForSelector(".swap-link");

const logs: string[] = [];
page.on("console", (msg) => logs.push(msg.text()));

await Promise.all([
page.waitForResponse((res) => res.status() === 500),
page.click(".swap-link"),
]);
},
);
});