Skip to content
Open
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
19 changes: 18 additions & 1 deletion platform/loader-html-hydrate/src/hydrater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export class PlasmicHtmlHydrater {
element.getAttribute("data-plasmic-prefetched-query-data") || "{}"
);

// Read page params data attributes for dynamic route support.
// Note: pageRoute is read but not passed to hydrateFromElement because
// the loader-react package doesn't support it in the hydration flow.
// This means $ctx.params and $ctx.query will work, but $ctx.pageRoute
// and $ctx.pagePath won't be available after hydration.
const _pageRoute =
element.getAttribute("data-plasmic-page-route") || undefined;
const pageParams = JSON.parse(
element.getAttribute("data-plasmic-page-params") || "{}"
);
const pageQuery = JSON.parse(
element.getAttribute("data-plasmic-page-query") || "{}"
);

const loader = initPlasmicLoader({
projects: [
{
Expand All @@ -52,7 +66,10 @@ export class PlasmicHtmlHydrater {
globalVariants,
componentProps,
prefetchedQueryData,
}
// Pass page params if present (for dynamic route support)
...(Object.keys(pageParams).length > 0 && { pageParams }),
...(Object.keys(pageQuery).length > 0 && { pageQuery }),
} as any // Type assertion needed - npm package types don't include pageParams/pageQuery
);
element.setAttribute("data-plasmic-hydrating", "false");
element.setAttribute("data-plasmic-hydrated", "true");
Expand Down
22 changes: 20 additions & 2 deletions platform/wab/src/wab/server/loader/gen-html-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@plasmicapp/loader-react";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { PageParams } from "./page-params";

export async function genLoaderHtmlBundle(opts: {
projectId: string;
Expand All @@ -22,6 +23,9 @@ export async function genLoaderHtmlBundle(opts: {
prepass?: boolean;
componentProps?: any;
globalVariants?: GlobalVariantSpec[];
pageRoute?: string;
pageParams?: PageParams;
pageQuery?: PageParams;
}) {
const {
projectId,
Expand All @@ -33,6 +37,9 @@ export async function genLoaderHtmlBundle(opts: {
componentProps,
globalVariants,
prepass,
pageRoute,
pageParams,
pageQuery,
} = opts;

// Set the data host for data source operations during SSR prepass.
Expand Down Expand Up @@ -65,7 +72,9 @@ export async function genLoaderHtmlBundle(opts: {
prefetchedData: data,
componentProps,
globalVariants,
}
pageParams,
pageQuery,
} as any // Type assertion needed - npm package types don't include pageParams/pageQuery
)
: undefined;

Expand All @@ -77,7 +86,9 @@ export async function genLoaderHtmlBundle(opts: {
componentProps,
globalVariants,
prefetchedQueryData,
}
pageParams,
pageQuery,
} as any // Type assertion needed - npm package types don't include pageParams/pageQuery
);

const outerElement = React.createElement(
Expand All @@ -97,6 +108,13 @@ export async function genLoaderHtmlBundle(opts: {
hydrate && embedHydrate && prepass
? JSON.stringify(prefetchedQueryData || {})
: "",
"data-plasmic-page-route": hydrate ? pageRoute || "" : "",
"data-plasmic-page-params": hydrate
? JSON.stringify(pageParams || {})
: "",
"data-plasmic-page-query": hydrate
? JSON.stringify(pageQuery || {})
: "",
dangerouslySetInnerHTML: { __html: innerHtml },
}),
hydrate &&
Expand Down
196 changes: 196 additions & 0 deletions platform/wab/src/wab/server/loader/page-params.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { BadRequestError } from "@/wab/shared/ApiErrors/errors";
import {
getInvalidParamKeys,
isValidParamsObject,
isValidParamValue,
isValidRoutePattern,
parsePageParams,
parsePageQuery,
parsePageRoute,
} from "./page-params";

describe("page-params", () => {
describe("isValidRoutePattern", () => {
it("returns true for valid route patterns", () => {
expect(isValidRoutePattern("/")).toBe(true);
expect(isValidRoutePattern("/products")).toBe(true);
expect(isValidRoutePattern("/products/[slug]")).toBe(true);
expect(isValidRoutePattern("/products/[...catchall]")).toBe(true);
expect(isValidRoutePattern("/a/b/c/[id]")).toBe(true);
});

it("returns false for invalid route patterns", () => {
expect(isValidRoutePattern("")).toBe(false);
expect(isValidRoutePattern("products")).toBe(false);
expect(isValidRoutePattern("products/[slug]")).toBe(false);
});
});

describe("isValidParamValue", () => {
it("returns true for valid param values", () => {
expect(isValidParamValue("hello")).toBe(true);
expect(isValidParamValue("")).toBe(true);
expect(isValidParamValue([])).toBe(true);
expect(isValidParamValue(["a", "b", "c"])).toBe(true);
});

it("returns false for invalid param values", () => {
expect(isValidParamValue(123)).toBe(false);
expect(isValidParamValue(null)).toBe(false);
expect(isValidParamValue(undefined)).toBe(false);
expect(isValidParamValue({})).toBe(false);
expect(isValidParamValue([1, 2, 3])).toBe(false);
expect(isValidParamValue(["a", 1])).toBe(false);
});
});

describe("isValidParamsObject", () => {
it("returns true for valid params objects", () => {
expect(isValidParamsObject({})).toBe(true);
expect(isValidParamsObject({ slug: "hello" })).toBe(true);
expect(isValidParamsObject({ slug: "hello", id: "123" })).toBe(true);
expect(isValidParamsObject({ catchall: ["a", "b"] })).toBe(true);
expect(isValidParamsObject({ slug: "hello", parts: ["a", "b"] })).toBe(
true
);
});

it("returns false for invalid params objects", () => {
expect(isValidParamsObject(null)).toBe(false);
expect(isValidParamsObject(undefined)).toBe(false);
expect(isValidParamsObject("string")).toBe(false);
expect(isValidParamsObject([])).toBe(false);
expect(isValidParamsObject([{ slug: "hello" }])).toBe(false);
expect(isValidParamsObject({ slug: 123 })).toBe(false);
expect(isValidParamsObject({ slug: null })).toBe(false);
});
});

describe("getInvalidParamKeys", () => {
it("returns empty array for valid params", () => {
expect(getInvalidParamKeys({})).toEqual([]);
expect(getInvalidParamKeys({ slug: "hello" })).toEqual([]);
expect(getInvalidParamKeys({ slug: ["a", "b"] })).toEqual([]);
});

it("returns keys with invalid values", () => {
expect(getInvalidParamKeys({ slug: 123 })).toEqual(["slug"]);
expect(getInvalidParamKeys({ a: "valid", b: 123, c: null })).toEqual([
"b",
"c",
]);
});
});

describe("parsePageRoute", () => {
it("returns undefined for empty/null/undefined input", () => {
expect(parsePageRoute()).toBeUndefined();
expect(parsePageRoute(undefined)).toBeUndefined();
expect(parsePageRoute(null)).toBeUndefined();
expect(parsePageRoute("")).toBeUndefined();
});

it("parses valid route patterns", () => {
expect(parsePageRoute("/")).toBe("/");
expect(parsePageRoute("/products")).toBe("/products");
expect(parsePageRoute("/products/[slug]")).toBe("/products/[slug]");
expect(parsePageRoute("/[...catchall]")).toBe("/[...catchall]");
});

it("throws BadRequestError for non-string input", () => {
expect(() => parsePageRoute(123)).toThrow(BadRequestError);
expect(() => parsePageRoute({})).toThrow(BadRequestError);
expect(() => parsePageRoute([])).toThrow(BadRequestError);
});

it("throws BadRequestError for routes not starting with /", () => {
expect(() => parsePageRoute("products")).toThrow(BadRequestError);
expect(() => parsePageRoute("products/[slug]")).toThrow(BadRequestError);
});
});

describe("parsePageParams", () => {
it("returns undefined for empty/null/undefined input", () => {
expect(parsePageParams()).toBeUndefined();
expect(parsePageParams(undefined)).toBeUndefined();
expect(parsePageParams(null)).toBeUndefined();
expect(parsePageParams("")).toBeUndefined();
});

it("parses valid JSON params", () => {
expect(parsePageParams("{}")).toEqual({});
expect(parsePageParams('{"slug":"hello"}')).toEqual({ slug: "hello" });
expect(parsePageParams('{"slug":"hello","id":"123"}')).toEqual({
slug: "hello",
id: "123",
});
expect(parsePageParams('{"catchall":["a","b","c"]}')).toEqual({
catchall: ["a", "b", "c"],
});
});

it("throws BadRequestError for non-string input", () => {
expect(() => parsePageParams(123)).toThrow(BadRequestError);
expect(() => parsePageParams({})).toThrow(BadRequestError);
});

it("throws BadRequestError for invalid JSON", () => {
expect(() => parsePageParams("{invalid}")).toThrow(BadRequestError);
expect(() => parsePageParams("not json")).toThrow(BadRequestError);
});

it("throws BadRequestError for non-object JSON", () => {
expect(() => parsePageParams("[]")).toThrow(BadRequestError);
expect(() => parsePageParams('"string"')).toThrow(BadRequestError);
expect(() => parsePageParams("123")).toThrow(BadRequestError);
expect(() => parsePageParams("null")).toThrow(BadRequestError);
});

it("throws BadRequestError for invalid param values", () => {
expect(() => parsePageParams('{"slug":123}')).toThrow(BadRequestError);
expect(() => parsePageParams('{"slug":null}')).toThrow(BadRequestError);
expect(() => parsePageParams('{"slug":[1,2,3]}')).toThrow(BadRequestError);
});

it("includes invalid key names in error message", () => {
expect(() => parsePageParams('{"validKey":"ok","badKey":123}')).toThrow(
/badKey/
);
});
});

describe("parsePageQuery", () => {
it("returns undefined for empty/null/undefined input", () => {
expect(parsePageQuery()).toBeUndefined();
expect(parsePageQuery(undefined)).toBeUndefined();
expect(parsePageQuery(null)).toBeUndefined();
expect(parsePageQuery("")).toBeUndefined();
});

it("parses valid JSON query params", () => {
expect(parsePageQuery("{}")).toEqual({});
expect(parsePageQuery('{"q":"search"}')).toEqual({ q: "search" });
expect(parsePageQuery('{"tags":["a","b"]}')).toEqual({
tags: ["a", "b"],
});
});

it("throws BadRequestError for non-string input", () => {
expect(() => parsePageQuery(123)).toThrow(BadRequestError);
expect(() => parsePageQuery({})).toThrow(BadRequestError);
});

it("throws BadRequestError for invalid JSON", () => {
expect(() => parsePageQuery("{invalid}")).toThrow(BadRequestError);
});

it("throws BadRequestError for non-object JSON", () => {
expect(() => parsePageQuery("[]")).toThrow(BadRequestError);
expect(() => parsePageQuery("null")).toThrow(BadRequestError);
});

it("throws BadRequestError for invalid query values", () => {
expect(() => parsePageQuery('{"page":123}')).toThrow(BadRequestError);
});
});
});
Loading