Skip to content
Draft
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"livereload": "^0.9.1",
"node-html-parser": "^7.0.1",
"rollup": "^4.26.0",
"urlpattern-polyfill": "^10.1.0",
"wc-compiler": "~0.18.0"
},
"devDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { generateCompilation } from "./lifecycles/compile.js";
async function run(command) {
process.env.__GWD_COMMAND__ = command;

if (!globalThis.URLPattern) {
await import("urlpattern-polyfill");
}

try {
console.info(`Running Greenwood with the ${command} command.`);
const compilation = await generateCompilation();
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/lib/api-route-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function responseAsObject(response) {
};
}

async function executeRouteModule({ href, request }) {
async function executeRouteModule({ href, request, props }) {
const { body, headers = {}, method, url } = request;
const contentType = headers["content-type"] || "";
// @ts-expect-error see https://github.com/microsoft/TypeScript/issues/42866
Expand All @@ -47,11 +47,16 @@ async function executeRouteModule({ href, request }) {
header: headers,
body: format,
}),
{
props: {
...props,
},
},
);

parentPort.postMessage(await responseAsObject(response));
}

parentPort.on("message", async (task) => {
await executeRouteModule(task);
parentPort.on("message", async (task, props) => {
await executeRouteModule(task, props);
});
8 changes: 7 additions & 1 deletion packages/cli/src/lib/execute-route-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ async function executeRouteModule({
htmlContents = null,
scripts = [],
request,
props,
contentOptions = {},
}) {
const data = {
Expand Down Expand Up @@ -35,10 +36,15 @@ async function executeRouteModule({

if (body) {
if (module.default) {
const { html } = await renderToString(new URL(moduleUrl), false, { request, compilation });
const { html } = await renderToString(new URL(moduleUrl), false, {
request,
compilation,
props,
});

data.body = html;
} else if (getBody) {
// TODO do we need to pass props here too?
data.body = await getBody(compilation, page, request);
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/lib/ssr-route-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ async function executeModule({
scripts = "[]",
request,
contentOptions = "{}",
props,
}) {
const { executeRouteModule } = await import(executeModuleUrl);
const data = await executeRouteModule({
Expand All @@ -22,6 +23,7 @@ async function executeModule({
scripts: JSON.parse(scripts),
request,
contentOptions: JSON.parse(contentOptions),
props,
});

parentPort.postMessage(data);
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/src/lib/url-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
function getDynamicSegmentsFromRoute({ route, relativePagePath, extension }) {
const dynamicRoute = route.replace("[", ":").replace("]", "");
const segmentKey = relativePagePath
.split("/")
[relativePagePath.split("/").length - 1].replace(extension, "")
.replace("[", "")
.replace("]", "")
.replace(".", "");

return { segmentKey, dynamicRoute };
}

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

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

function getPropsFromSegment(segment, pathname) {
return new URLPattern({ pathname: segment.pathname }).exec(`https://example.com${pathname}`)
.pathname.groups;
}

export {
getDynamicSegmentsFromRoute,
getMatchingDynamicApiRoute,
getPropsFromSegment,
getMatchingDynamicSsrRoute,
};
4 changes: 2 additions & 2 deletions packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ async function bundleSsrPages(compilation, optimizePlugins) {

const moduleUrl = new URL('${relativeDepth}${pagesPathDiff}${pagePath.replace("./", "")}', import.meta.url);

export async function handler(request) {
export async function handler(request, props) {
const compilation = JSON.parse(\`${JSON.stringify({
...compilation,
graph: pruneGraph(compilation.graph),
Expand All @@ -371,7 +371,7 @@ async function bundleSsrPages(compilation, optimizePlugins) {
const page = JSON.parse(\`${JSON.stringify(pruneGraph([page])[0])
.replace(/\\"/g, "&quote")
.replace(/\\n/g, "")}\`);
const data = await executeRouteModule({ moduleUrl, compilation, page, request, contentOptions: { body: true } });
const data = await executeRouteModule({ moduleUrl, compilation, page, request, contentOptions: { body: true }, props });
let staticHtml = \`${staticHtml}\`;

if (data.body) {
Expand Down
26 changes: 25 additions & 1 deletion packages/cli/src/lifecycles/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import fm from "front-matter";
import { checkResourceExists, requestAsObject } from "../lib/resource-utils.js";
import { activeFrontmatterKeys } from "../lib/content-utils.js";
import { getDynamicSegmentsFromRoute } from "../lib/url-utils.js";
import { Worker } from "node:worker_threads";

function getLabelFromRoute(_route) {
Expand All @@ -26,7 +27,12 @@ function getLabelFromRoute(_route) {
}

function getIdFromRelativePathPath(relativePathPath, extension) {
return relativePathPath.replace(`.${extension}`, "").replace("./", "").replace(/\//g, "-");
return relativePathPath
.replace(`.${extension}`, "")
.replace("./", "")
.replace(/\//g, "-")
.replace("[", "-")
.replace("]", "-");
}

function trackCollectionsForPage(page, collections) {
Expand Down Expand Up @@ -104,6 +110,12 @@ const generateGraph = async (compilation) => {

// TODO should API routes be run in isolation mode like SSR pages?
const { isolation } = await import(filenameUrl).then((module) => module);
const { segmentKey, dynamicRoute } = getDynamicSegmentsFromRoute({
route,
relativePagePath,
extension,
basePath,
});

/*
* API Properties (per route)
Expand All @@ -122,6 +134,8 @@ const generateGraph = async (compilation) => {
outputHref: new URL(relativePagePath, outputDir).href.replace(`.${extension}`, ".js"),
route: `${basePath}${route}`,
isolation,
segment:
dynamicRoute.indexOf(":") > 0 ? { key: segmentKey, pathname: dynamicRoute } : null,
});
} else if (isPage) {
let root = filename
Expand Down Expand Up @@ -245,6 +259,14 @@ const generateGraph = async (compilation) => {
* hydration: if this page needs hydration support
* servePage: signal that this is a custom page file type (static | dynamic)
*/

const { segmentKey, dynamicRoute } = getDynamicSegmentsFromRoute({
route,
relativePagePath,
extension,
basePath,
});

const page = {
id: decodeURIComponent(getIdFromRelativePathPath(relativePagePath, extension)),
label: decodeURIComponent(label),
Expand All @@ -264,6 +286,8 @@ const generateGraph = async (compilation) => {
isolation,
hydration,
servePage: isCustom,
segment:
dynamicRoute.indexOf(":") > 0 ? { key: segmentKey, pathname: dynamicRoute } : null,
};

pages.push(page);
Expand Down
42 changes: 36 additions & 6 deletions packages/cli/src/lifecycles/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {
transformKoaRequestIntoStandardRequest,
requestAsObject,
} from "../lib/resource-utils.js";
import {
getMatchingDynamicApiRoute,
getMatchingDynamicSsrRoute,
getPropsFromSegment,
} from "../lib/url-utils.js";
import { Readable } from "node:stream";
import { Worker } from "node:worker_threads";

Expand Down Expand Up @@ -360,12 +365,30 @@ async function getHybridServer(compilation) {
app.use(async (ctx) => {
try {
const url = new URL(`http://localhost:${config.port}${ctx.url}`);
const { pathname } = url;
const matchingRoute = graph.find((node) => node.route === url.pathname) || { data: {} };
const isApiRoute = manifest.apis.has(url.pathname);
const request = transformKoaRequestIntoStandardRequest(url, ctx.request);
const matchingApiRouteWithSegment = getMatchingDynamicApiRoute(
compilation.manifest.apis,
pathname,
);
const matchingRouteWithSegment =
getMatchingDynamicSsrRoute(compilation.graph, pathname) || {};

if (
!config.prerender &&
(matchingRoute.isSSR || matchingRouteWithSegment.isSSR) &&
!matchingRoute.prerender
) {
const entryPointUrl = new URL(
matchingRoute?.outputHref ?? matchingRouteWithSegment.outputHref,
);
const props =
matchingRouteWithSegment && matchingRouteWithSegment.segment
? getPropsFromSegment(matchingRouteWithSegment.segment, pathname)
: undefined;

if (!config.prerender && matchingRoute.isSSR && !matchingRoute.prerender) {
const entryPointUrl = new URL(matchingRoute.outputHref);
let html;

if (matchingRoute.isolation || isolationMode) {
Expand Down Expand Up @@ -393,12 +416,13 @@ async function getHybridServer(compilation) {
routeModuleUrl: entryPointUrl.href,
request,
compilation: JSON.stringify(compilation),
props,
});
});
} else {
// @ts-expect-error see https://github.com/microsoft/TypeScript/issues/42866
const { handler } = await import(entryPointUrl);
const response = await handler(request, compilation);
const response = await handler(request, props);

html = Readable.from(response.body);
}
Expand All @@ -407,8 +431,13 @@ async function getHybridServer(compilation) {
ctx.message = "OK";
ctx.set("Content-Type", "text/html");
ctx.status = 200;
} else if (isApiRoute) {
const apiRoute = manifest.apis.get(url.pathname);
} else if (isApiRoute || matchingApiRouteWithSegment) {
const apiRoute = manifest.apis.get(matchingApiRouteWithSegment ?? pathname);
const props =
matchingRouteWithSegment && apiRoute.segment
? getPropsFromSegment(apiRoute.segment, pathname)
: undefined;

const entryPointUrl = new URL(apiRoute.outputHref);
let body, status, headers, statusText;

Expand Down Expand Up @@ -439,12 +468,13 @@ async function getHybridServer(compilation) {
worker.postMessage({
href: entryPointUrl.href,
request: req,
props,
});
});
} else {
// @ts-expect-error see https://github.com/microsoft/TypeScript/issues/42866
const { handler } = await import(entryPointUrl);
const response = await handler(request);
const response = await handler(request, { props });

body = response.body;
status = response.status;
Expand Down
28 changes: 24 additions & 4 deletions packages/cli/src/plugins/resource/plugin-api-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*
*/
import { requestAsObject } from "../../lib/resource-utils.js";
import { getMatchingDynamicApiRoute, getPropsFromSegment } from "../../lib/url-utils.js";
import { Worker } from "node:worker_threads";

class ApiRoutesResource {
Expand All @@ -13,15 +14,34 @@ class ApiRoutesResource {
}

async shouldServe(url) {
const { basePath } = this.compilation.config;
const { protocol, pathname } = url;

return protocol.startsWith("http") && this.compilation.manifest.apis.has(pathname);
if (!protocol.startsWith("http") || !pathname.startsWith(`${basePath}/api/`)) {
return;
}

const matchingRouteWithSegment = getMatchingDynamicApiRoute(
this.compilation.manifest.apis,
pathname,
);

return matchingRouteWithSegment || this.compilation.manifest.apis.has(pathname);
}

async serve(url, request) {
const api = this.compilation.manifest.apis.get(url.pathname);
const { pathname } = url;
const matchingRouteWithSegment = getMatchingDynamicApiRoute(
this.compilation.manifest.apis,
pathname,
);
const api = this.compilation.manifest.apis.get(matchingRouteWithSegment ?? pathname);
const apiUrl = new URL(api.pageHref);
const href = apiUrl.href;
const props =
matchingRouteWithSegment && api.segment
? getPropsFromSegment(api.segment, pathname)
: undefined;

if (process.env.__GWD_COMMAND__ === "develop") {
const workerUrl = new URL("../../lib/api-route-worker.js", import.meta.url);
Expand All @@ -40,7 +60,7 @@ class ApiRoutesResource {
}
});

worker.postMessage({ href, request: req });
worker.postMessage({ href, request: req, props });
});
const { headers, body, status, statusText } = response;

Expand All @@ -52,7 +72,7 @@ class ApiRoutesResource {
} else {
const { handler } = await import(href);

return await handler(request);
return await handler(request, props);
}
}
}
Expand Down
Loading