diff --git a/.changeset/cuddly-ears-collect.md b/.changeset/cuddly-ears-collect.md new file mode 100644 index 000000000000..a06229efd3fc --- /dev/null +++ b/.changeset/cuddly-ears-collect.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +chore: rename `--script-path` to `--outfile` for `wrangler pages functions build` command. diff --git a/.changeset/spicy-knives-rush.md b/.changeset/spicy-knives-rush.md new file mode 100644 index 000000000000..6c3f58950e98 --- /dev/null +++ b/.changeset/spicy-knives-rush.md @@ -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. diff --git a/CODEOWNERS b/CODEOWNERS index 38f0606a9e02..f8a62fd3a0d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,3 @@ # Global owners * @threepointone @petebacondarwin +/packages/wrangler/pages/ @GregBrimble diff --git a/examples/pages-functions-app/CHANGELOG.md b/examples/pages-functions-app/CHANGELOG.md deleted file mode 100644 index 79ccb4daf6df..000000000000 --- a/examples/pages-functions-app/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# example-pages-functions-app - -## null - -### Patch Changes - -- [#617](https://github.com/cloudflare/wrangler2/pull/617) [`8c98c00`](https://github.com/cloudflare/wrangler2/commit/8c98c008a8f80ab92bff6425312a170d7e582533) Thanks [@GregBrimble](https://github.com/GregBrimble)! - Added a Pages Functions example project for tests diff --git a/examples/pages-functions-app/functions/mounted-plugin/_middleware.ts b/examples/pages-functions-app/functions/mounted-plugin/_middleware.ts new file mode 100644 index 000000000000..46eaed7b9679 --- /dev/null +++ b/examples/pages-functions-app/functions/mounted-plugin/_middleware.ts @@ -0,0 +1,3 @@ +import examplePlugin from "../../../pages-plugin-example"; + +export const onRequest = examplePlugin({ footerText: "Set from a Plugin!" }); diff --git a/examples/pages-functions-app/public/index.html b/examples/pages-functions-app/public/index.html index 9f735a6307ae..a289365236c7 100644 --- a/examples/pages-functions-app/public/index.html +++ b/examples/pages-functions-app/public/index.html @@ -1 +1,6 @@ -

Hello, world!

+ + + +

Hello, world!

+ + diff --git a/examples/pages-functions-app/public/some-asset.html b/examples/pages-functions-app/public/some-asset.html index 08d2e2408532..281c968f4329 100644 --- a/examples/pages-functions-app/public/some-asset.html +++ b/examples/pages-functions-app/public/some-asset.html @@ -1 +1,6 @@ -

An asset

+ + + +

An asset

+ + diff --git a/examples/pages-functions-app/tests/index.test.ts b/examples/pages-functions-app/tests/index.test.ts index 31b286b0cb36..87e6e1af2831 100644 --- a/examples/pages-functions-app/tests/index.test.ts +++ b/examples/pages-functions-app/tests/index.test.ts @@ -93,4 +93,20 @@ describe("Pages Functions", () => { const text = await response.text(); expect(text).toContain("

An asset

"); }); + + 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(""); + + // Fixed page + response = await waitUntilReady( + "http://localhost:8789/mounted-plugin/fixed" + ); + text = await response.text(); + expect(text).toContain("I'm a fixed response"); + }); }); diff --git a/examples/pages-plugin-example/.gitignore b/examples/pages-plugin-example/.gitignore new file mode 100644 index 000000000000..f72018060151 --- /dev/null +++ b/examples/pages-plugin-example/.gitignore @@ -0,0 +1 @@ +/index.js \ No newline at end of file diff --git a/examples/pages-plugin-example/functions/_middleware.ts b/examples/pages-plugin-example/functions/_middleware.ts new file mode 100644 index 000000000000..7158a0516b69 --- /dev/null +++ b/examples/pages-plugin-example/functions/_middleware.ts @@ -0,0 +1,20 @@ +class BodyHandler { + footerText: string; + + constructor({ footerText }) { + this.footerText = footerText; + } + + element(element) { + // Don't actually set HTML like this! + element.append(``, { html: true }); + } +} + +export const onRequest = async ({ next, pluginArgs }) => { + const response = await next(); + + return new HTMLRewriter() + .on("body", new BodyHandler({ footerText: pluginArgs.footerText })) + .transform(response); +}; diff --git a/examples/pages-plugin-example/functions/fixed.ts b/examples/pages-plugin-example/functions/fixed.ts new file mode 100644 index 000000000000..b55d9e8a122b --- /dev/null +++ b/examples/pages-plugin-example/functions/fixed.ts @@ -0,0 +1,3 @@ +export const onRequest = () => { + return new Response("I'm a fixed response"); +}; diff --git a/examples/pages-plugin-example/package.json b/examples/pages-plugin-example/package.json new file mode 100644 index 000000000000..6a3c6503ccda --- /dev/null +++ b/examples/pages-plugin-example/package.json @@ -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" + } +} diff --git a/package.json b/package.json index c7b733922c77..a8ee20a72262 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/packages/wrangler/pages/functions/buildPlugin.ts b/packages/wrangler/pages/functions/buildPlugin.ts new file mode 100644 index 000000000000..fca2b03ed1e7 --- /dev/null +++ b/packages/wrangler/pages/functions/buildPlugin.ts @@ -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(); + } + }); + }, + }, + ], + }); +} diff --git a/packages/wrangler/pages/functions/filepath-routing.test.ts b/packages/wrangler/pages/functions/filepath-routing.test.ts index 45e502f71617..95d75ac3a36c 100644 --- a/packages/wrangler/pages/functions/filepath-routing.test.ts +++ b/packages/wrangler/pages/functions/filepath-routing.test.ts @@ -143,6 +143,7 @@ describe("filepath-routing", () => { "module": Array [ "authors/[authorId]/todos/[todoId].ts:onRequestPost", ], + "mountPath": "/base/authors/:authorId/todos", "routePath": "/base/authors/:authorId/todos/:todoId", }, Object { @@ -150,6 +151,7 @@ describe("filepath-routing", () => { "module": Array [ "cats/[[breed]]/blah.ts:onRequestPost", ], + "mountPath": "/base/cats/:breed*", "routePath": "/base/cats/:breed*/blah", }, Object { @@ -157,6 +159,7 @@ describe("filepath-routing", () => { "module": Array [ "cats/[[breed]]/[[name]].ts:onRequestPost", ], + "mountPath": "/base/cats/:breed*", "routePath": "/base/cats/:breed*/:name*", }, Object { @@ -164,6 +167,7 @@ describe("filepath-routing", () => { "module": Array [ "todos/[id].ts:onRequestDelete", ], + "mountPath": "/base/todos", "routePath": "/base/todos/:id", }, Object { @@ -171,6 +175,7 @@ describe("filepath-routing", () => { "module": Array [ "todos/[id].ts:onRequestPost", ], + "mountPath": "/base/todos", "routePath": "/base/todos/:id", }, Object { @@ -178,6 +183,7 @@ describe("filepath-routing", () => { "module": Array [ "books/[[title]].ts:onRequestPost", ], + "mountPath": "/base/books", "routePath": "/base/books/:title*", }, Object { @@ -185,6 +191,7 @@ describe("filepath-routing", () => { "module": Array [ "bar.ts:onRequestDelete", ], + "mountPath": "/base/", "routePath": "/base/bar", }, Object { @@ -192,6 +199,7 @@ describe("filepath-routing", () => { "module": Array [ "bar.ts:onRequestPut", ], + "mountPath": "/base/", "routePath": "/base/bar", }, Object { @@ -199,6 +207,7 @@ describe("filepath-routing", () => { "module": Array [ "foo.ts:onRequestGet", ], + "mountPath": "/base/", "routePath": "/base/foo", }, Object { @@ -206,6 +215,7 @@ describe("filepath-routing", () => { "module": Array [ "foo.ts:onRequestPost", ], + "mountPath": "/base/", "routePath": "/base/foo", }, ], @@ -218,6 +228,7 @@ describe("filepath-routing", () => { function routeConfig(routePath: string, method?: string): RouteConfig { return { routePath: toUrlPath(routePath), + mountPath: toUrlPath("/"), method: method as HTTPMethod, }; } diff --git a/packages/wrangler/pages/functions/filepath-routing.ts b/packages/wrangler/pages/functions/filepath-routing.ts index 4b33bb1a83b0..1f1a07350fae 100644 --- a/packages/wrangler/pages/functions/filepath-routing.ts +++ b/packages/wrangler/pages/functions/filepath-routing.ts @@ -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); @@ -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}`, diff --git a/packages/wrangler/pages/functions/routes.ts b/packages/wrangler/pages/functions/routes.ts index de2607ee325f..c214223dfc22 100755 --- a/packages/wrangler/pages/functions/routes.ts +++ b/packages/wrangler/pages/functions/routes.ts @@ -21,6 +21,7 @@ export function isHTTPMethod( export type RoutesCollection = Array<{ routePath: UrlPath; + mountPath: UrlPath; method?: HTTPMethod; modules: string[]; middlewares: string[]; @@ -33,6 +34,7 @@ export type Config = { export type RouteConfig = { routePath: UrlPath; + mountPath: UrlPath; method?: HTTPMethod; middleware?: string | string[]; module?: string | string[]; @@ -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), @@ -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(", ")}], diff --git a/packages/wrangler/pages/functions/template-plugin.ts b/packages/wrangler/pages/functions/template-plugin.ts new file mode 100644 index 000000000000..8f62cd62075a --- /dev/null +++ b/packages/wrangler/pages/functions/template-plugin.ts @@ -0,0 +1,146 @@ +import { match } from "path-to-regexp"; +import type { HTTPMethod } from "./routes"; + +/* TODO: Grab these from @cloudflare/workers-types instead */ +type Params

= Record; + +type EventContext = { + request: Request; + functionPath: string; + waitUntil: (promise: Promise) => void; + next: (input?: Request | string, init?: RequestInit) => Promise; + env: Env & { ASSETS: { fetch: typeof fetch } }; + params: Params

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

; + data: Data; + pluginArgs: PluginArgs; +}; + +declare type PagesFunction< + Env = unknown, + P extends string = string, + Data extends Record = Record +> = (context: EventContext) => Response | Promise; + +declare type PagesPluginFunction< + Env = unknown, + P extends string = string, + Data extends Record = Record, + PluginArgs = unknown +> = ( + context: EventPluginContext +) => Response | Promise; +/* end @cloudflare/workers-types */ + +type RouteHandler = { + routePath: string; + mountPath: string; + method?: HTTPMethod; + modules: PagesFunction[]; + middlewares: PagesFunction[]; +}; + +// inject `routes` via ESBuild +declare const routes: RouteHandler[]; + +function* executeRequest(request: Request, 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 mountMatcher = match(route.mountPath, { end: false }); + const matchResult = routeMatcher(relativePathname); + const mountMatchResult = mountMatcher(relativePathname); + if (matchResult && mountMatchResult) { + for (const handler of route.middlewares.flat()) { + yield { + handler, + params: matchResult.params as Params, + path: mountMatchResult.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 mountMatcher = match(route.mountPath, { end: false }); + const matchResult = routeMatcher(relativePathname); + const mountMatchResult = mountMatcher(relativePathname); + if (matchResult && mountMatchResult && route.modules.length) { + for (const handler of route.modules.flat()) { + yield { + handler, + params: matchResult.params as Params, + path: matchResult.path, + }; + } + break; + } + } +} + +export default function (pluginArgs) { + const onRequest: PagesPluginFunction = async (workerContext) => { + let { request } = workerContext; + const { env, next, data } = workerContext; + + const url = new URL(request.url); + const relativePathname = + url.pathname.split(workerContext.functionPath)[1] || "/"; + + const handlerIterator = executeRequest(request, 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, + 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 { + return next(); + } + }; + + return pluginNext(); + }; + + return onRequest; +} diff --git a/packages/wrangler/pages/functions/template-worker.ts b/packages/wrangler/pages/functions/template-worker.ts index 41d88d878b05..61e3513ca327 100644 --- a/packages/wrangler/pages/functions/template-worker.ts +++ b/packages/wrangler/pages/functions/template-worker.ts @@ -6,6 +6,7 @@ type Params

= Record; type EventContext = { request: Request; + functionPath: string; waitUntil: (promise: Promise) => void; next: (input?: Request | string, init?: RequestInit) => Promise; env: Env & { ASSETS: { fetch: typeof fetch } }; @@ -22,6 +23,7 @@ declare type PagesFunction< type RouteHandler = { routePath: string; + mountPath: string; method?: HTTPMethod; modules: PagesFunction[]; middlewares: PagesFunction[]; @@ -42,7 +44,7 @@ type WorkerContext = { waitUntil: (promise: Promise) => void; }; -function* executeRequest(request: Request, _env: FetchEnv) { +function* executeRequest(request: Request) { const requestPath = new URL(request.url).pathname; // First, iterate through the routes (backwards) and execute "middlewares" on partial route matches @@ -52,12 +54,15 @@ function* executeRequest(request: Request, _env: FetchEnv) { } const routeMatcher = match(route.routePath, { end: false }); + const mountMatcher = match(route.mountPath, { end: false }); const matchResult = routeMatcher(requestPath); - if (matchResult) { + const mountMatchResult = mountMatcher(requestPath); + if (matchResult && mountMatchResult) { for (const handler of route.middlewares.flat()) { yield { handler, params: matchResult.params as Params, + path: mountMatchResult.path, }; } } @@ -70,12 +75,15 @@ function* executeRequest(request: Request, _env: FetchEnv) { } const routeMatcher = match(route.routePath, { end: true }); + const mountMatcher = match(route.mountPath, { end: false }); const matchResult = routeMatcher(requestPath); - if (matchResult && route.modules.length) { + const mountMatchResult = mountMatcher(requestPath); + if (matchResult && mountMatchResult && route.modules.length) { for (const handler of route.modules.flat()) { yield { handler, params: matchResult.params as Params, + path: matchResult.path, }; } break; @@ -85,7 +93,7 @@ function* executeRequest(request: Request, _env: FetchEnv) { export default { async fetch(request: Request, env: FetchEnv, workerContext: WorkerContext) { - const handlerIterator = executeRequest(request, env); + const handlerIterator = executeRequest(request); const data = {}; // arbitrary data the user can set between functions const next = async (input?: RequestInfo, init?: RequestInit) => { if (input !== undefined) { @@ -98,10 +106,11 @@ 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; + if (result.done === false) { + const { handler, params, path } = result.value; const context = { request: new Request(request.clone()), + functionPath: path, next, params, data, diff --git a/packages/wrangler/src/pages.tsx b/packages/wrangler/src/pages.tsx index cfa9da7bd045..352529a496b5 100644 --- a/packages/wrangler/src/pages.tsx +++ b/packages/wrangler/src/pages.tsx @@ -11,6 +11,7 @@ import Table from "ink-table"; import { getType } from "mime"; import React from "react"; import { format as timeagoFormat } from "timeago.js"; +import { buildPlugin } from "../pages/functions/buildPlugin"; import { buildWorker } from "../pages/functions/buildWorker"; import { generateConfigFromFileTree } from "../pages/functions/filepath-routing"; import { writeRoutesModule } from "../pages/functions/routes"; @@ -694,7 +695,7 @@ async function generateAssetsFetch(directory: string): Promise { const RUNNING_BUILDERS: BuildResult[] = []; async function buildFunctions({ - scriptPath, + outfile, outputConfigPath, functionsDirectory, minify = false, @@ -702,8 +703,9 @@ async function buildFunctions({ fallbackService = "ASSETS", watch = false, onEnd, + plugin = false, }: { - scriptPath: string; + outfile: string; outputConfigPath?: string; functionsDirectory: string; minify?: boolean; @@ -711,6 +713,7 @@ async function buildFunctions({ fallbackService?: string; watch?: boolean; onEnd?: () => void; + plugin?: boolean; }) { RUNNING_BUILDERS.forEach( (runningBuilder) => runningBuilder.stop && runningBuilder.stop() @@ -737,17 +740,30 @@ async function buildFunctions({ outfile: routesModule, }); - RUNNING_BUILDERS.push( - await buildWorker({ - routesModule, - outfile: scriptPath, - minify, - sourcemap, - fallbackService, - watch, - onEnd, - }) - ); + if (plugin) { + RUNNING_BUILDERS.push( + await buildPlugin({ + routesModule, + outfile, + minify, + sourcemap, + watch, + onEnd, + }) + ); + } else { + RUNNING_BUILDERS.push( + await buildWorker({ + routesModule, + outfile, + minify, + sourcemap, + fallbackService, + watch, + onEnd, + }) + ); + } } export const pages: BuilderCallback = (yargs) => { @@ -856,13 +872,13 @@ export const pages: BuilderCallback = (yargs) => { ); if (usingFunctions) { - const scriptPath = join(tmpdir(), "./functionsWorker.js"); + const outfile = join(tmpdir(), "./functionsWorker.js"); - console.log(`Compiling worker to "${scriptPath}"...`); + console.log(`Compiling worker to "${outfile}"...`); try { await buildFunctions({ - scriptPath, + outfile, functionsDirectory, sourcemap: true, watch: true, @@ -875,7 +891,7 @@ export const pages: BuilderCallback = (yargs) => { ignoreInitial: true, }).on("all", async () => { await buildFunctions({ - scriptPath, + outfile, functionsDirectory, sourcemap: true, watch: true, @@ -884,7 +900,7 @@ export const pages: BuilderCallback = (yargs) => { }); miniflareArgs = { - scriptPath, + scriptPath: outfile, }; } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -1042,7 +1058,7 @@ export const pages: BuilderCallback = (yargs) => { description: "The directory of Pages Functions", }) .options({ - "script-path": { + outfile: { type: "string", default: "_worker.js", description: "The location of the output Worker script", @@ -1074,28 +1090,35 @@ export const pages: BuilderCallback = (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", + }, }) .epilogue(pagesBetaWarning), async ({ directory, - "script-path": scriptPath, + outfile, "output-config-path": outputConfigPath, minify, sourcemap, fallbackService, watch, + plugin, }) => { // Beta message for `wrangler pages ` usage console.log(pagesBetaWarning); await buildFunctions({ - scriptPath, + outfile, outputConfigPath, functionsDirectory: directory, minify, sourcemap, fallbackService, watch, + plugin, }); } ) diff --git a/packages/wrangler/tsconfig.json b/packages/wrangler/tsconfig.json index 85ae70074487..bfcac2ec72b8 100644 --- a/packages/wrangler/tsconfig.json +++ b/packages/wrangler/tsconfig.json @@ -10,6 +10,7 @@ "vendor", "*-dist", "pages/functions/template-worker.ts", + "pages/functions/template-plugin.ts", "templates" ] }