Skip to content

Commit

Permalink
Pages plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
GregBrimble committed Feb 7, 2022
1 parent 2d0a520 commit d152684
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 12 deletions.
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
---

Adds a `--plugin` option to `wrangler pages functions build` which compiles a Pages Plugin. More information about these WIP plugins can be found here: https://github.com/gregbrimble/pages-plugins. This wrangler build is required for both the development of, and inclusion of, plugins.
68 changes: 68 additions & 0 deletions packages/wrangler/pages/functions/buildPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { resolve } from "path";
import { build } from "esbuild";

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

export function buildPlugin({
routesModule,
outfile = "bundle.js",
minify = false,
sourcemap = false,
watch = false,
pluginName,
pluginAssetsDirectory,
onEnd = () => {},
}: Options) {
const entryPoint = resolve(
__dirname,
"../pages/functions/template-plugin.ts"
);

return build({
entryPoints: [entryPoint],
inject: [routesModule],
bundle: true,
format: "esm",
target: "esnext",
outfile,
minify,
sourcemap,
watch,
allowOverwrite: true,
define: {
__PLUGIN_NAME__: JSON.stringify(pluginName),
__PLUGIN_ASSETS_DIRECTORY__: JSON.stringify(pluginAssetsDirectory),
},
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();
}
});
},
},
],
});
}
165 changes: 165 additions & 0 deletions packages/wrangler/pages/functions/template-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { match } from "path-to-regexp";
import type { HTTPMethod } from "./routes";

/* TODO: Grab these from @cloudflare/workers-types instead */
type Params<P extends string = string> = Record<P, string | string[]>;

type EventContext<Env, P extends string, Data> = {
request: Request;
functionPath: string;
waitUntil: (promise: Promise<unknown>) => void;
next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
env: Env & { ASSETS: { fetch: typeof fetch } };
params: Params<P>;
data: Data;
};

type EventPluginContext<Env, P extends string, Data, PluginArgs> = {
request: Request;
functionPath: string;
waitUntil: (promise: Promise<unknown>) => void;
next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
_next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
env: Env & { ASSETS: { fetch: typeof fetch } };
params: Params<P>;
data: Data;
pluginArgs: PluginArgs;
};

declare type PagesFunction<
Env = unknown,
P extends string = string,
Data extends Record<string, unknown> = Record<string, unknown>
> = (context: EventContext<Env, P, Data>) => Response | Promise<Response>;

declare type PagesPluginFunction<
Env = unknown,
P extends string = string,
Data extends Record<string, unknown> = Record<string, unknown>,
PluginArgs = unknown
> = (
context: EventPluginContext<Env, P, Data, PluginArgs>
) => Response | Promise<Response>;
/* end @cloudflare/workers-types */

type RouteHandler = {
routePath: string;
method?: HTTPMethod;
modules: PagesFunction[];
middlewares: PagesFunction[];
};

// inject `routes` via ESBuild
declare const routes: RouteHandler[];
// define `__PLUGIN_NAME__` and `__PLUGIN_ASSETS_DIRECTORY__` via ESBuild
declare const __PLUGIN_NAME__: string;
declare const __PLUGIN_ASSETS_DIRECTORY__: string;

// expect an ASSETS fetcher binding pointing to the asset-server stage
type FetchEnv = {
[name: string]: { fetch: typeof fetch };
ASSETS: { fetch: typeof fetch };
};

function* executeRequest(
request: Request,
_env: FetchEnv,
relativePathname: string
) {
// First, iterate through the routes (backwards) and execute "middlewares" on partial route matches
for (const route of [...routes].reverse()) {
if (route.method && route.method !== request.method) {
continue;
}

const routeMatcher = match(route.routePath, { end: false });
const matchResult = routeMatcher(relativePathname);
if (matchResult) {
for (const handler of route.middlewares.flat()) {
yield {
handler,
params: matchResult.params as Params,
path: matchResult.path,
};
}
}
}

// Then look for the first exact route match and execute its "modules"
for (const route of routes) {
if (route.method && route.method !== request.method) {
continue;
}

const routeMatcher = match(route.routePath, { end: true });
const matchResult = routeMatcher(relativePathname);
if (matchResult && route.modules.length) {
for (const handler of route.modules.flat()) {
yield {
handler,
params: matchResult.params as Params,
path: matchResult.path,
};
}
break;
}
}
}

export const name = __PLUGIN_NAME__;
export const assetsDirectory = __PLUGIN_ASSETS_DIRECTORY__;

export default function (pluginArgs) {
const onRequest: PagesPluginFunction = async (workerContext) => {
let { request } = workerContext;
const { env, next, data } = workerContext;

const url = new URL(request.url);
const basePath = workerContext.functionPath;
const relativePathname = `/${url.pathname.split(basePath)[1]}`;

const handlerIterator = executeRequest(request, env, relativePathname);
const pluginNext = async (input?: RequestInfo, init?: RequestInit) => {
if (input !== undefined) {
request = new Request(input, init);
}

const result = handlerIterator.next();
// Note we can't use `!result.done` because this doesn't narrow to the correct type
if (result.done == false) {
const { handler, params, path } = result.value;
const context = {
request,
functionPath: workerContext.functionPath + path,
next: pluginNext,
_next: next,
params,
data,
pluginArgs,
env,
waitUntil: workerContext.waitUntil.bind(workerContext),
};

const response = await handler(context);

// https://fetch.spec.whatwg.org/#null-body-status
return new Response(
[101, 204, 205, 304].includes(response.status) ? null : response.body,
response
);
} else if (__PLUGIN_ASSETS_DIRECTORY__ !== undefined && "ASSETS" in env) {
request = new Request(
`http://fakehost/cdn-cgi/pages-plugins/${__PLUGIN_NAME__}${relativePathname}`,
request
);
return env.ASSETS.fetch(request);
} else {
return next();
}
};

return pluginNext();
};

return onRequest;
}
6 changes: 5 additions & 1 deletion packages/wrangler/pages/functions/template-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Params<P extends string = string> = Record<P, string | string[]>;

type EventContext<Env, P extends string, Data> = {
request: Request;
functionPath: string;
waitUntil: (promise: Promise<unknown>) => void;
next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
env: Env & { ASSETS: { fetch: typeof fetch } };
Expand Down Expand Up @@ -58,6 +59,7 @@ function* executeRequest(request: Request, _env: FetchEnv) {
yield {
handler,
params: matchResult.params as Params,
path: matchResult.path,
};
}
}
Expand All @@ -76,6 +78,7 @@ function* executeRequest(request: Request, _env: FetchEnv) {
yield {
handler,
params: matchResult.params as Params,
path: matchResult.path,
};
}
break;
Expand All @@ -95,9 +98,10 @@ export default {
const result = handlerIterator.next();
// Note we can't use `!result.done` because this doesn't narrow to the correct type
if (result.done == false) {
const { handler, params } = result.value;
const { handler, params, path } = result.value;
const context = {
request: new Request(request.clone()),
functionPath: path,
next,
params,
data,
Expand Down
56 changes: 45 additions & 11 deletions packages/wrangler/src/pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { watch } from "chokidar";
import { getType } from "mime";
import open from "open";
import { buildWorker } from "../pages/functions/buildWorker";
import { buildPlugin } from "../pages/functions/buildPlugin";
import type { Config } from "../pages/functions/routes";
import { writeRoutesModule } from "../pages/functions/routes";
import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
import { writeRoutesModule } from "../pages/functions/routes";
import { toUrlPath } from "./paths";
Expand Down Expand Up @@ -651,6 +654,7 @@ async function buildFunctions({
fallbackService = "ASSETS",
watch = false,
onEnd,
plugin = false,
}: {
scriptPath: string;
outputConfigPath?: string;
Expand All @@ -660,6 +664,7 @@ async function buildFunctions({
fallbackService?: string;
watch?: boolean;
onEnd?: () => void;
plugin?: boolean;
}) {
RUNNING_BUILDERS.forEach(
(runningBuilder) => runningBuilder.stop && runningBuilder.stop()
Expand All @@ -686,17 +691,39 @@ async function buildFunctions({
outfile: routesModule,
});

RUNNING_BUILDERS.push(
await buildWorker({
routesModule,
outfile: scriptPath,
minify,
sourcemap,
fallbackService,
watch,
onEnd,
})
);
if (plugin) {
const packageJSON = JSON.parse(readFileSync("package.json").toString());
const pluginName = packageJSON.name;
if (!pluginName) {
throw new Error("package.json must include a 'name' for the plugin.");
}
const pluginAssetsDirectory = packageJSON?.directories?.assets;

RUNNING_BUILDERS.push(
await buildPlugin({
routesModule,
outfile: scriptPath,
minify,
sourcemap,
watch,
onEnd,
pluginName,
pluginAssetsDirectory,
})
);
} else {
RUNNING_BUILDERS.push(
await buildWorker({
routesModule,
outfile: scriptPath,
minify,
sourcemap,
fallbackService,
watch,
onEnd,
})
);
}
}

export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
Expand Down Expand Up @@ -1015,6 +1042,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
description:
"Watch for changes to the functions and automatically rebuild the Worker script",
},
plugin: {
type: "boolean",
default: false,
description: "Build a plugin rather than a Worker script",
},
}),
async ({
directory,
Expand All @@ -1024,6 +1056,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
sourcemap,
fallbackService,
watch,
plugin,
}) => {
await buildFunctions({
scriptPath,
Expand All @@ -1033,6 +1066,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
sourcemap,
fallbackService,
watch,
plugin,
});
}
)
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"vendor",
"*-dist",
"pages/functions/template-worker.ts",
"pages/functions/template-plugin.ts",
"templates"
]
}

0 comments on commit d152684

Please sign in to comment.