Skip to content

Commit

Permalink
Pages Plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
GregBrimble committed Apr 20, 2022
1 parent 4a00910 commit ebc6699
Show file tree
Hide file tree
Showing 21 changed files with 366 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-ears-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

chore: rename `--script-path` to `--outfile` for `wrangler pages functions build` command.
5 changes: 5 additions & 0 deletions .changeset/spicy-knives-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

feature: Adds a `--plugin` option to `wrangler pages functions build` which compiles a Pages Plugin. More information about Pages Plugins can be found [here](https://developers.cloudflare.com/pages/platform/functions/plugins/). This wrangler build is required for both the development of, and inclusion of, plugins.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Global owners
* @threepointone @petebacondarwin
/packages/wrangler/pages/ @GregBrimble
7 changes: 0 additions & 7 deletions examples/pages-functions-app/CHANGELOG.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import examplePlugin from "../../../pages-plugin-example";

export const onRequest = examplePlugin({ footerText: "Set from a Plugin!" });
7 changes: 6 additions & 1 deletion examples/pages-functions-app/public/index.html
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<h1>Hello, world!</h1>
<!DOCTYPE html>
<html>
<body>
<h1>Hello, world!</h1>
</body>
</html>
7 changes: 6 additions & 1 deletion examples/pages-functions-app/public/some-asset.html
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<h1>An asset</h1>
<!DOCTYPE html>
<html>
<body>
<h1>An asset</h1>
</body>
</html>
18 changes: 17 additions & 1 deletion examples/pages-functions-app/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawn } from "child_process";
import { spawn, spawnSync } from "child_process";
import * as path from "path";
import { fetch } from "undici";
import type { ChildProcess } from "child_process";
Expand Down Expand Up @@ -93,4 +93,20 @@ describe("Pages Functions", () => {
const text = await response.text();
expect(text).toContain("<h1>An asset</h1>");
});

it("can mount a plugin", async () => {
// Middleware
let response = await waitUntilReady(
"http://localhost:8789/mounted-plugin/some-page"
);
let text = await response.text();
expect(text).toContain("<footer>Set from a Plugin!</footer>");

// Fixed page
response = await waitUntilReady(
"http://localhost:8789/mounted-plugin/fixed"
);
text = await response.text();
expect(text).toContain("I'm a fixed response");
});
});
1 change: 1 addition & 0 deletions examples/pages-plugin-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/index.js
20 changes: 20 additions & 0 deletions examples/pages-plugin-example/functions/_middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class BodyHandler {
footerText: string;

constructor({ footerText }) {
this.footerText = footerText;
}

element(element) {
// Don't actually set HTML like this!
element.append(`<footer>${this.footerText}</footer>`, { html: true });
}
}

export const onRequest = async ({ next, pluginArgs }) => {
const response = await next();

return new HTMLRewriter()
.on("body", new BodyHandler({ footerText: pluginArgs.footerText }))
.transform(response);
};
3 changes: 3 additions & 0 deletions examples/pages-plugin-example/functions/fixed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const onRequest = () => {
return new Response("I'm a fixed response");
};
13 changes: 13 additions & 0 deletions examples/pages-plugin-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "pages-plugin-example",
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.js",
"index.d.ts",
"tsconfig.json"
],
"scripts": {
"build": "npx wrangler pages functions build --plugin --outfile=index.js"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"check:type": "npm run check:type --workspaces --if-present",
"check:lint": "eslint \"packages/**/*.[tj]s?(x)\" --cache --cache-strategy content --max-warnings=0",
"check:format": "prettier packages/** .changeset --check --ignore-unknown",
"build": "npm run build --workspace=wrangler --workspace=jest-environment-wrangler",
"build": "npm run build --workspace=wrangler --workspace=jest-environment-wrangler && npm run build --workspace=pages-plugin-example",
"test": "npm run test --workspaces --if-present",
"prettify": "prettier packages/** --write --ignore-unknown"
},
Expand Down
55 changes: 55 additions & 0 deletions packages/wrangler/pages/functions/buildPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { resolve } from "node:path";
import { build } from "esbuild";

type Options = {
routesModule: string;
outfile: string;
minify?: boolean;
sourcemap?: boolean;
watch?: boolean;
onEnd?: () => void;
};

export function buildPlugin({
routesModule,
outfile = "bundle.js",
minify = false,
sourcemap = false,
watch = false,
onEnd = () => {},
}: Options) {
return build({
entryPoints: [resolve(__dirname, "../pages/functions/template-plugin.ts")],
inject: [routesModule],
bundle: true,
format: "esm",
target: "esnext",
outfile,
minify,
sourcemap,
watch,
allowOverwrite: true,
plugins: [
{
name: "wrangler notifier and monitor",
setup(pluginBuild) {
pluginBuild.onEnd((result) => {
if (result.errors.length > 0) {
console.error(
`${result.errors.length} error(s) and ${result.warnings.length} warning(s) when compiling Worker.`
);
} else if (result.warnings.length > 0) {
console.warn(
`${result.warnings.length} warning(s) when compiling Worker.`
);
onEnd();
} else {
console.log("Compiled Worker successfully.");
onEnd();
}
});
},
},
],
});
}
11 changes: 11 additions & 0 deletions packages/wrangler/pages/functions/filepath-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,69 +143,79 @@ describe("filepath-routing", () => {
"module": Array [
"authors/[authorId]/todos/[todoId].ts:onRequestPost",
],
"mountPath": "/base/authors/:authorId/todos",
"routePath": "/base/authors/:authorId/todos/:todoId",
},
Object {
"method": "POST",
"module": Array [
"cats/[[breed]]/blah.ts:onRequestPost",
],
"mountPath": "/base/cats/:breed*",
"routePath": "/base/cats/:breed*/blah",
},
Object {
"method": "POST",
"module": Array [
"cats/[[breed]]/[[name]].ts:onRequestPost",
],
"mountPath": "/base/cats/:breed*",
"routePath": "/base/cats/:breed*/:name*",
},
Object {
"method": "DELETE",
"module": Array [
"todos/[id].ts:onRequestDelete",
],
"mountPath": "/base/todos",
"routePath": "/base/todos/:id",
},
Object {
"method": "POST",
"module": Array [
"todos/[id].ts:onRequestPost",
],
"mountPath": "/base/todos",
"routePath": "/base/todos/:id",
},
Object {
"method": "POST",
"module": Array [
"books/[[title]].ts:onRequestPost",
],
"mountPath": "/base/books",
"routePath": "/base/books/:title*",
},
Object {
"method": "DELETE",
"module": Array [
"bar.ts:onRequestDelete",
],
"mountPath": "/base/",
"routePath": "/base/bar",
},
Object {
"method": "PUT",
"module": Array [
"bar.ts:onRequestPut",
],
"mountPath": "/base/",
"routePath": "/base/bar",
},
Object {
"method": "GET",
"module": Array [
"foo.ts:onRequestGet",
],
"mountPath": "/base/",
"routePath": "/base/foo",
},
Object {
"method": "POST",
"module": Array [
"foo.ts:onRequestPost",
],
"mountPath": "/base/",
"routePath": "/base/foo",
},
],
Expand All @@ -218,6 +228,7 @@ describe("filepath-routing", () => {
function routeConfig(routePath: string, method?: string): RouteConfig {
return {
routePath: toUrlPath(routePath),
mountPath: toUrlPath("/"),
method: method as HTTPMethod,
};
}
8 changes: 8 additions & 0 deletions packages/wrangler/pages/functions/filepath-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export async function generateConfigFromFileTree({
let routePath = path
.relative(baseDir, filepath)
.slice(0, -ext.length);
let mountPath = path.dirname(routePath);

if (isIndexFile || isMiddlewareFile) {
routePath = path.dirname(routePath);
Expand All @@ -61,17 +62,24 @@ export async function generateConfigFromFileTree({
if (routePath === ".") {
routePath = "";
}
if (mountPath === ".") {
mountPath = "";
}

routePath = `${baseURL}/${routePath}`;
mountPath = `${baseURL}/${mountPath}`;

routePath = routePath.replace(/\[\[([^\]]+)\]\]/g, ":$1*"); // transform [[id]] => :id*
routePath = routePath.replaceAll(/\[([^\]]+)\]/g, ":$1"); // transform [id] => :id
mountPath = mountPath.replace(/\[\[([^\]]+)\]\]/g, ":$1*"); // transform [[id]] => :id*
mountPath = mountPath.replaceAll(/\[([^\]]+)\]/g, ":$1"); // transform [id] => :id

// These are used as module specifiers so UrlPaths are okay to use even on Windows
const modulePath = toUrlPath(path.relative(baseDir, filepath));

const routeEntry: RouteConfig = {
routePath: toUrlPath(routePath),
mountPath: toUrlPath(mountPath),
method: method.toUpperCase() as HTTPMethod,
[isMiddlewareFile ? "middleware" : "module"]: [
`${modulePath}:${exportName}`,
Expand Down
7 changes: 6 additions & 1 deletion packages/wrangler/pages/functions/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function isHTTPMethod(

export type RoutesCollection = Array<{
routePath: UrlPath;
mountPath: UrlPath;
method?: HTTPMethod;
modules: string[];
middlewares: string[];
Expand All @@ -33,6 +34,7 @@ export type Config = {

export type RouteConfig = {
routePath: UrlPath;
mountPath: UrlPath;
method?: HTTPMethod;
middleware?: string | string[];
module?: string | string[];
Expand Down Expand Up @@ -113,9 +115,11 @@ export function parseConfig(config: Config, baseDir: string) {
});
}

for (const { routePath, method, ...props } of config.routes ?? []) {
for (const { routePath, mountPath, method, ...props } of config.routes ??
[]) {
routes.push({
routePath,
mountPath,
method,
middlewares: parseModuleIdentifiers(props.middleware),
modules: parseModuleIdentifiers(props.module),
Expand All @@ -141,6 +145,7 @@ export const routes = [
.map(
(route) => ` {
routePath: "${route.routePath}",
mountPath: "${route.mountPath}",
method: "${route.method}",
middlewares: [${route.middlewares.join(", ")}],
modules: [${route.modules.join(", ")}],
Expand Down
Loading

0 comments on commit ebc6699

Please sign in to comment.