Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ff46df8
feature(cli): #1622 get static paths and static params
thescientist13 Apr 25, 2026
7f0a1ff
feature(cli): #1622 make sure get static paths usage does not emit an…
thescientist13 Apr 25, 2026
c5def15
feature(cli): #1622 add test cases for get body
thescientist13 Apr 25, 2026
aedcc0b
feature(cli): #1622 use segment key lookup for init static params
thescientist13 Apr 25, 2026
4f9aba6
feature(cli): #1622 update specs per rebase
thescientist13 May 28, 2026
bfb5356
feature(cli): #1622 fix prod server not correctly matching static pat…
thescientist13 Jun 6, 2026
390d350
feature(cli): #1622 prerender refactoring to support hybrid rendering
thescientist13 Jun 7, 2026
9fd946f
feature(cli): #1622 guard adapter plugins from adapting prerendered r…
thescientist13 Jun 13, 2026
5678cd5
feature(cli): #1622 add more cases to adapter test suites
thescientist13 Jun 13, 2026
32bbd6e
feature(plugins): #1622 support get static paths and props for lit re…
thescientist13 Jun 13, 2026
2569609
chore(cli): #1622 update generic adapter test case for dynamic routing
thescientist13 Jun 13, 2026
b14c774
chore(cli): #1622 console log clean up and refactoring
thescientist13 Jun 14, 2026
53e0bdc
feature(cli): #1622 support base path routing
thescientist13 Jun 20, 2026
a9846dc
chore(cli): #1622 refactoring
thescientist13 Jun 20, 2026
08d47eb
chore(cli): #1622 clean up comments
thescientist13 Jun 20, 2026
cd726b7
docs(plugins): #1622 document get static paths caveat for puppeteer p…
thescientist13 Jun 21, 2026
5b7611f
feature(types): #1622 add type for get static paths and params
thescientist13 Jun 21, 2026
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
3 changes: 2 additions & 1 deletion packages/cli/src/commands/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ const runProductionBuild = async (compilation) => {
const adapterPlugin = compilation.config.plugins.find((plugin) => plugin.type === "adapter")
? compilation.config.plugins.find((plugin) => plugin.type === "adapter").provider(compilation)
: null;
const pagesWithStaticPaths = compilation.graph.filter((page) => page.staticPaths);

if (prerender) {
if (prerender || pagesWithStaticPaths.length > 0) {
// start any of the user's server plugins if needed
const servers = [
...compilation.config.plugins
Expand Down
8 changes: 3 additions & 5 deletions packages/cli/src/commands/serve.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { getStaticServer, getHybridServer } from "../lifecycles/serve.js";
import { checkResourceExists } from "../lib/resource-utils.js";
import { getDynamicPages } from "../lib/graph-utils.js";

const runProdServer = async (compilation) => {
const { basePath, port } = compilation.config;
const postfixSlash = basePath === "" ? "" : "/";
const hasApisDir = await checkResourceExists(compilation.context.apisDir);
const hasDynamicRoutes = compilation.graph.find((page) => page.isSSR && !page.prerender);
const server =
(hasDynamicRoutes && !compilation.config.prerender) || hasApisDir
? getHybridServer
: getStaticServer;
const hasDynamicRoutes = getDynamicPages(compilation).length > 0;
const server = hasDynamicRoutes || hasApisDir ? getHybridServer : getStaticServer;

(await server(compilation)).listen(port, () => {
console.info(`Started server at http://localhost:${port}${basePath}${postfixSlash}`);
Expand Down
40 changes: 37 additions & 3 deletions packages/cli/src/lib/execute-route-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ async function executeRouteModule({
htmlContents = null,
scripts = [],
request,
params,
params = {},
contentOptions = {},
}) {
const data = {
layout: null,
body: null,
frontmatter: null,
html: null,
staticPaths: null,
hasStaticParams: null,
};

if (prerender) {
Expand All @@ -25,15 +27,47 @@ async function executeRouteModule({
data.html = html;
} else {
const module = await import(moduleUrl).then((module) => module);
const { body, layout, frontmatter } = contentOptions;
const { body, layout, frontmatter, statics } = contentOptions;
const {
prerender = false,
prerender = null,
getLayout = null,
getBody = null,
getFrontmatter = null,
isolation,
} = module;

if (statics && module.getStaticPaths) {
data.staticPaths = await module.getStaticPaths();
}

if (statics && module.getStaticParams) {
data.hasStaticParams = true;
}

if (params) {
if (page.staticPaths) {
const staticPaths = page.staticPaths ?? [];

if (page.hasStaticParams) {
const initParams = {
...params,
...staticPaths.find(
(staticPath) => staticPath.params[page.segment.key] === params[page.segment.key],
),
};

const staticParams = module.getStaticParams
? await module.getStaticParams(initParams)
: {};

params = {
...params,
...staticParams,
};
}
}
}

if (body) {
if (module.default) {
const { html } = await renderToString(new URL(moduleUrl), false, {
Expand Down
55 changes: 55 additions & 0 deletions packages/cli/src/lib/graph-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// pages that are pure SSR
function getDynamicPages(compilation) {
const { config, graph } = compilation;

// would be nice to do this without the extra conditional (good first issue)
return graph.filter((page) => {
let isSsrRoute = page.isSSR && !page.staticPaths && page.prerender !== true;

if (isSsrRoute && config.prerender && page.prerender !== false) {
isSsrRoute = false;
}

return isSsrRoute;
});
}

// pages that emit an HTML file
function getStaticPages(compilation) {
const { config, graph } = compilation;

return graph.filter(
(page) =>
!page.isSSR ||
(page.isSSR && page.prerender) ||
(page.isSSR && page.prerender !== false && config.prerender) ||
page.staticPaths,
);
}

// get a page by route; including getStaticPaths or dynamic SSR pages
function getMatchingPageByRoute(compilation, route) {
const { graph, config } = compilation;

return graph.find((page) => {
return (
// exact match
page.route === route ||
// dynamic route
(page.segment &&
new URLPattern({ pathname: `${config.basePath}${page.segment.pathname}` }).test(
`https://example.com${route}`,
)) ||
// getStaticPaths
(page.hasStaticParams &&
page.staticPaths.find((path) => {
const { segment } = page;
const staticRoute = route.replace(`[${segment.key}]`, path.params[segment.key]);

return `${config.basePath}${staticRoute}` === route;
}))
);
});
}

export { getDynamicPages, getStaticPages, getMatchingPageByRoute };
37 changes: 25 additions & 12 deletions packages/cli/src/lib/url-utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// get the dynamic segments from a dynamic route, e.g. pages/blog/[slug].js
function getDynamicSegmentsFromRoute({ route, relativePagePath, extension }) {
const dynamicRoute = route.replace("[", ":").replace("]", "");
const segmentKey = relativePagePath
Expand All @@ -10,36 +11,48 @@ function getDynamicSegmentsFromRoute({ route, relativePagePath, extension }) {
return { segmentKey, dynamicRoute };
}

function getMatchingDynamicApiRoute(apis, pathname) {
// all API routes
function getMatchingDynamicApiRoute(apis, route) {
return Array.from(apis.keys()).find((key) => {
const route = apis.get(key);
const page = apis.get(key);
return (
route.segment &&
new URLPattern({ pathname: `${route.segment.pathname}*` }).test(
`https://example.com${pathname}`,
)
page.segment &&
new URLPattern({ pathname: `${page.segment.pathname}*` }).test(`https://example.com${route}`)
);
});
}

function getMatchingDynamicSsrRoute(graph, pathname) {
// pure SSR routes
function getMatchingDynamicSsrRoute(compilation, route) {
const { graph, config } = compilation;

return graph.find((node) => {
return (
(pathname !== "/404/") !== "/404/" &&
route !== "/404/" &&
node.segment &&
new URLPattern({ pathname: node.segment.pathname }).test(`https://example.com${pathname}`)
new URLPattern({ pathname: `${config.basePath}${node.segment.pathname}` }).test(
`https://example.com${route}`,
)
);
});
}

function getParamsFromSegment(segment, pathname) {
return new URLPattern({ pathname: segment.pathname }).exec(`https://example.com${pathname}`)
.pathname.groups;
// get params for dynamic routes from URLPattern based segment extraction
function getParamsFromSegment(compilation, segment, route) {
return new URLPattern({ pathname: `${compilation.config.basePath}${segment.pathname}` }).exec(
`https://example.com${route}`,
)?.pathname?.groups;
}

// get the full route for a static path
function getStaticRouteFromDynamicRoute(dynamicStaticPath, segment, route) {
return `${route.replace(`[${segment.key}]`, dynamicStaticPath.params[segment.key])}`;
}

export {
getDynamicSegmentsFromRoute,
getMatchingDynamicApiRoute,
getParamsFromSegment,
getMatchingDynamicSsrRoute,
getStaticRouteFromDynamicRoute,
};
104 changes: 74 additions & 30 deletions packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import path from "node:path";
import { rollup } from "rollup";
import { pruneGraph } from "../lib/content-utils.js";
import { asyncForEach } from "../lib/async-utils.js";
import { getDynamicPages, getStaticPages } from "../lib/graph-utils.js";
import { getStaticRouteFromDynamicRoute } from "../lib/url-utils.js";

async function interceptPage(url, request, plugins, body) {
let response = new Response(body, {
Expand Down Expand Up @@ -113,43 +115,85 @@ async function cleanUpResources(compilation) {
});
}

// we could try and refactor / consolidate here some of the duplicate logic
async function optimizeStaticPages(compilation, plugins) {
const { scratchDir, outputDir } = compilation.context;

const pages = compilation.graph.filter(
(page) =>
!page.isSSR || (page.isSSR && page.prerender) || (page.isSSR && compilation.config.prerender),
);
const pages = getStaticPages(compilation);

await asyncForEach(pages, async (page) => {
const { route, outputHref } = page;
const outputDirUrl = new URL(outputHref.replace("index.html", "").replace("404.html", ""));
const url = new URL(`http://localhost:${compilation.config.port}${route}`);
const contents = await fs.readFile(
new URL(`./${outputHref.replace(outputDir.href, "")}`, scratchDir),
"utf-8",
);
const headers = new Headers({ "Content-Type": "text/html" });
let response = new Response(contents, { headers });

if (!(await checkResourceExists(outputDirUrl))) {
await fs.mkdir(outputDirUrl, {
recursive: true,
});
}
const { outputHref, route, segment, staticPaths } = page;

if (staticPaths) {
for (const staticPath of staticPaths) {
const staticRoute = getStaticRouteFromDynamicRoute(staticPath, segment, route);
const outputDirUrl = new URL(
outputHref
.replace(`[${segment.key}]`, staticPath.params[segment.key])
.replace("index.html", ""),
);
const url = new URL(`http://localhost:${compilation.config.port}${staticRoute}`);
const contents = await fs.readFile(
new URL(
`./${outputHref.replace(outputDir.href, "").replace(`[${segment.key}]`, staticPath.params[segment.key])}`,
scratchDir,
),
"utf-8",
);
const headers = new Headers({ "Content-Type": "text/html" });
let response = new Response(contents, { headers });

if (!(await checkResourceExists(outputDirUrl))) {
await fs.mkdir(outputDirUrl, {
recursive: true,
});
}

for (const plugin of plugins) {
if (plugin.shouldOptimize && (await plugin.shouldOptimize(url, response.clone()))) {
const currentResponse = await plugin.optimize(url, response.clone());
for (const plugin of plugins) {
if (plugin.shouldOptimize && (await plugin.shouldOptimize(url, response.clone()))) {
const currentResponse = await plugin.optimize(url, response.clone());

response = mergeResponse(response.clone(), currentResponse.clone());
response = mergeResponse(response.clone(), currentResponse.clone());
}
}

// clean up optimization markers
const body = (await response.text()).replace(/data-gwd-opt=".*?[a-z]"/g, "");

await fs.writeFile(
new URL(outputHref.replace(`[${segment.key}]`, staticPath.params[segment.key])),
body,
);
}
} else {
const { route, outputHref } = page;
const outputDirUrl = new URL(outputHref.replace("index.html", "").replace("404.html", ""));
const url = new URL(`http://localhost:${compilation.config.port}${route}`);
const contents = await fs.readFile(
new URL(`./${outputHref.replace(outputDir.href, "")}`, scratchDir),
"utf-8",
);
const headers = new Headers({ "Content-Type": "text/html" });
let response = new Response(contents, { headers });

if (!(await checkResourceExists(outputDirUrl))) {
await fs.mkdir(outputDirUrl, {
recursive: true,
});
}
}

// clean up optimization markers
const body = (await response.text()).replace(/data-gwd-opt=".*?[a-z]"/g, "");
for (const plugin of plugins) {
if (plugin.shouldOptimize && (await plugin.shouldOptimize(url, response.clone()))) {
const currentResponse = await plugin.optimize(url, response.clone());

await fs.writeFile(new URL(outputHref), body);
response = mergeResponse(response.clone(), currentResponse.clone());
}
}

// clean up optimization markers
const body = (await response.text()).replace(/data-gwd-opt=".*?[a-z]"/g, "");

await fs.writeFile(new URL(outputHref), body);
}
});
}

Expand Down Expand Up @@ -280,11 +324,11 @@ async function bundleApiRoutes(compilation) {

async function bundleSsrPages(compilation, optimizePlugins) {
const { context, config } = compilation;
const ssrPages = compilation.graph.filter((page) => page.isSSR && !page.prerender);
const ssrPages = getDynamicPages(compilation);
const ssrPrerenderPagesRouteMapper = {};
const input = [];

if (!config.prerender && ssrPages.length > 0) {
if (ssrPages.length > 0) {
const { executeModuleUrl } = config.plugins
.find((plugin) => plugin.type === "renderer")
.provider();
Expand Down
Loading
Loading