-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Extract Pages functions build logic into its own package #12027
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ascorbic
wants to merge
29
commits into
main
Choose a base branch
from
pages-functions-package
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,047
−0
Open
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
31442b8
[pages-functions] Add @cloudflare/pages-functions package
ascorbic eebf5c1
Add scripts/deps.ts for external dependencies
ascorbic 3ea65c8
Add @cloudflare/pages-functions to validate-changesets test
ascorbic 3078968
Resolve path-to-regexp at codegen time, add runtime tests
ascorbic e437fac
Add pages-functions-test fixture
ascorbic c296541
Fix CI checks: add turbo.json, exclude runtime tests from lint/type
ascorbic 32327d7
Format _routes.json with tabs
ascorbic 066101c
Merge branch 'main' into pages-functions-package
ascorbic 71b9341
Accept projectDirectory, add CLI, update README
ascorbic 4ba0a87
Fix fixture to use direct node invocation for CLI
ascorbic b9b5e30
Update lockfile
ascorbic 36fee8d
Format _routes.json
ascorbic a9f328b
Gitignore generated files in fixture
ascorbic 0ec790b
Use node:util parseArgs for CLI argument parsing
ascorbic 430704c
Add esbuild to dependencies
ascorbic 7dc9eb5
Remove functionsDir option - directory is always 'functions'
ascorbic 4b203f8
Remove outdated fixture README
ascorbic 9bb2109
Use bare import for path-to-regexp instead of absolute path
ascorbic cc8dc03
Update README to clarify path-to-regexp dependency
ascorbic 5b930e5
Remove path-to-regexp dep, document it as user requirement
ascorbic cd3f55b
Remove unused deps script
ascorbic 8a569e9
Add path-to-regexp to devDependencies for tests
ascorbic f304d4c
Switch to bundler moduleResolution, remove .js extensions
ascorbic a394015
Revert to NodeNext moduleResolution for Node ESM compatibility
ascorbic b7f1f77
Add deps.ts for external dependency check
ascorbic 74ba005
Merge branch 'main' into pages-functions-package
ascorbic 1643b04
Fix reserved keyword regex to properly group alternation
ascorbic b2f66b2
Merge branch 'main' into pages-functions-package
ascorbic 22dfdee
Lock
ascorbic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| --- | ||
| "@cloudflare/pages-functions": minor | ||
| --- | ||
|
|
||
| Add @cloudflare/pages-functions package | ||
|
|
||
| Extracts the Pages functions-to-worker compilation logic from wrangler into a standalone package. | ||
|
|
||
| This enables converting a Pages functions directory into a deployable worker entrypoint, which is needed for the Autoconfig Pages work where `wrangler deploy` should "just work" for Pages projects. | ||
|
|
||
| ```ts | ||
| import { compileFunctions } from "@cloudflare/pages-functions"; | ||
|
|
||
| const result = await compileFunctions("./functions"); | ||
| // result.code - generated worker entrypoint | ||
| // result.routesJson - _routes.json for Pages deployment | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Pages Functions Test Fixture | ||
|
|
||
| Test fixture for `@cloudflare/pages-functions` package. | ||
|
|
||
| ## Usage | ||
|
|
||
| From the workers-sdk root: | ||
|
|
||
| ```bash | ||
| # Build the pages-functions package first | ||
| pnpm --filter @cloudflare/pages-functions build | ||
|
|
||
| # Install deps for this fixture | ||
| pnpm --filter pages-functions-test install | ||
|
|
||
| # Compile functions -> worker, then bundle, then run dev | ||
| pnpm --filter pages-functions-test dev | ||
| ``` | ||
|
|
||
| ## What it does | ||
|
|
||
| 1. `build.mjs` uses `compileFunctions()` to compile `functions/` into `dist/worker.js` | ||
| 2. esbuild bundles `dist/worker.js` (with path-to-regexp) into `dist/bundled.js` | ||
| 3. wrangler runs the bundled worker | ||
|
|
||
| ## Test endpoints | ||
|
|
||
| - `GET /` - Index route | ||
| - `GET /api/hello` - Static API route | ||
| - `POST /api/hello` - POST handler | ||
| - `GET /api/:id` - Dynamic route | ||
| - `PUT /api/:id` - Update handler | ||
| - `DELETE /api/:id` - Delete handler | ||
|
|
||
| All routes go through the root middleware which adds `X-Middleware: active` header. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "version": 1, | ||
| "description": "Generated by @cloudflare/pages-functions", | ||
| "include": [ | ||
| "/*" | ||
| ], | ||
| "exclude": [] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export const onRequest = async (context) => { | ||
| const response = await context.next(); | ||
| response.headers.set("X-Middleware", "active"); | ||
| return response; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export const onRequestGet = (context) => { | ||
| const { id } = context.params; | ||
| return Response.json({ message: `Getting item ${id}` }); | ||
| }; | ||
|
|
||
| export const onRequestPut = async (context) => { | ||
| const { id } = context.params; | ||
| const body = await context.request.json(); | ||
| return Response.json({ message: `Updating item ${id}`, data: body }); | ||
| }; | ||
|
|
||
| export const onRequestDelete = (context) => { | ||
| const { id } = context.params; | ||
| return Response.json({ message: `Deleted item ${id}` }); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export const onRequestGet = () => { | ||
| return Response.json({ message: "Hello from GET /api/hello" }); | ||
| }; | ||
|
|
||
| export const onRequestPost = async (context) => { | ||
| const body = await context.request.json(); | ||
| return Response.json({ | ||
| message: "Hello from POST /api/hello", | ||
| received: body, | ||
| }); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const onRequest = () => { | ||
| return new Response("Hello from the index!"); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "@fixture/pages-functions-test", | ||
| "private": true, | ||
| "scripts": { | ||
| "build": "pages-functions", | ||
| "dev": "pages-functions && wrangler dev" | ||
| }, | ||
| "dependencies": { | ||
| "@cloudflare/pages-functions": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "wrangler": "workspace:*" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "$schema": "http://turbo.build/schema.json", | ||
| "extends": ["//"], | ||
| "tasks": { | ||
| "build": { | ||
| "dependsOn": ["@cloudflare/pages-functions#build"], | ||
| "outputs": ["dist/**"] | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "pages-functions-test", | ||
| "main": "dist/worker.js", | ||
| "compatibility_date": "2026-01-20", | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # @cloudflare/pages-functions | ||
|
|
||
| Compile a Pages project's functions directory into a deployable worker entrypoint. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| npm install @cloudflare/pages-functions | ||
| ``` | ||
|
|
||
| ## CLI Usage | ||
|
|
||
| ```bash | ||
| # Compile the current project (looks for ./functions) | ||
| pages-functions | ||
|
|
||
| # Compile a specific project | ||
| pages-functions ./my-project | ||
|
|
||
| # Custom output location | ||
| pages-functions -o worker.js | ||
|
|
||
| # See all options | ||
| pages-functions --help | ||
| ``` | ||
|
|
||
| ### CLI Options | ||
|
|
||
| ``` | ||
| Usage: pages-functions [options] [project-dir] | ||
|
|
||
| Arguments: | ||
| project-dir Path to the project root (default: ".") | ||
|
|
||
| Options: | ||
| -o, --outfile <path> Output file for the worker entrypoint (default: "dist/worker.js") | ||
| --routes-json <path> Output path for _routes.json (default: "_routes.json") | ||
| --no-routes-json Don't generate _routes.json | ||
| --functions-dir <dir> Functions directory relative to project (default: "functions") | ||
| --base-url <url> Base URL for routes (default: "/") | ||
| --fallback-service <name> Fallback service binding name (default: "ASSETS") | ||
| -h, --help Show this help message | ||
| ``` | ||
|
|
||
| ## Programmatic API | ||
|
|
||
| ```typescript | ||
| import * as fs from "node:fs/promises"; | ||
| import { compileFunctions } from "@cloudflare/pages-functions"; | ||
|
|
||
| const result = await compileFunctions(".", { | ||
| fallbackService: "ASSETS", | ||
| }); | ||
|
|
||
| // Write the generated worker entrypoint | ||
| await fs.writeFile("dist/worker.js", result.code); | ||
|
|
||
| // Write _routes.json for Pages deployment | ||
| await fs.writeFile( | ||
| "_routes.json", | ||
| JSON.stringify(result.routesJson, null, "\t") | ||
| ); | ||
| ``` | ||
|
|
||
| ### `compileFunctions(projectDirectory, options?)` | ||
|
|
||
| Compiles a Pages project's functions directory into a worker entrypoint. | ||
|
|
||
| #### Parameters | ||
|
|
||
| - `projectDirectory` (string): Path to the project root (containing the functions directory) | ||
| - `options` (object, optional): | ||
| - `functionsDir` (string): Functions directory relative to project root. Default: `"functions"` | ||
| - `baseURL` (string): Base URL prefix for all routes. Default: `"/"` | ||
| - `fallbackService` (string): Fallback service binding name. Default: `"ASSETS"` | ||
|
|
||
| #### Returns | ||
|
|
||
| A `Promise<CompileResult>` with: | ||
|
|
||
| - `code` (string): Generated JavaScript worker entrypoint | ||
| - `routes` (RouteConfig[]): Parsed route configuration | ||
| - `routesJson` (RoutesJSONSpec): `_routes.json` content for Pages deployment | ||
|
|
||
| ## Project Structure | ||
|
|
||
| Your project should have a `functions` directory with file-based routing: | ||
|
|
||
| ``` | ||
| my-project/ | ||
| ├── functions/ | ||
| │ ├── index.ts # Handles / | ||
| │ ├── _middleware.ts # Middleware for all routes | ||
| │ └── api/ | ||
| │ ├── index.ts # Handles /api | ||
| │ ├── [id].ts # Handles /api/:id | ||
| │ └── [[catchall]].ts # Handles /api/* | ||
| ├── wrangler.jsonc | ||
| └── package.json | ||
| ``` | ||
|
|
||
| ### Route Parameters | ||
|
|
||
| - `[param]` - Dynamic parameter (e.g., `[id].ts` → `/api/:id`) | ||
| - `[[catchall]]` - Catch-all parameter (e.g., `[[path]].ts` → `/api/:path*`) | ||
|
|
||
| ### Handler Exports | ||
|
|
||
| Export handlers from your function files: | ||
|
|
||
| ```typescript | ||
| // Handle all methods | ||
| export const onRequest = (context) => new Response("Hello"); | ||
|
|
||
| // Handle specific methods | ||
| export const onRequestGet = (context) => new Response("GET"); | ||
| export const onRequestPost = (context) => new Response("POST"); | ||
| ``` | ||
|
|
||
| ### Middleware | ||
|
|
||
| Create a `_middleware.ts` file to run code before your handlers: | ||
|
|
||
| ```typescript | ||
| export const onRequest = async (context) => { | ||
| const response = await context.next(); | ||
| response.headers.set("X-Custom-Header", "value"); | ||
| return response; | ||
| }; | ||
| ``` | ||
|
|
||
| ## Generated Output | ||
|
|
||
| The generated code: | ||
|
|
||
| 1. Imports all function handlers from the functions directory | ||
| 2. Creates a route configuration array | ||
| 3. Includes the Pages Functions runtime (route matching, middleware execution) | ||
| 4. Exports a default handler that routes requests | ||
|
|
||
| The output imports `path-to-regexp` for route matching. When bundled by wrangler or esbuild, this dependency is resolved and included automatically. | ||
|
|
||
| ## License | ||
|
|
||
| MIT OR Apache-2.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { generateWorkerEntrypoint } from "../src/codegen.js"; | ||
| import type { RouteConfig } from "../src/types.js"; | ||
|
|
||
| describe("codegen", () => { | ||
| describe("generateWorkerEntrypoint", () => { | ||
| it("generates imports and routes array", () => { | ||
| const routes: RouteConfig[] = [ | ||
| { | ||
| routePath: "/api/:id", | ||
| mountPath: "/api", | ||
| method: "GET", | ||
| module: ["api/[id].ts:onRequestGet"], | ||
| }, | ||
| ]; | ||
|
|
||
| const code = generateWorkerEntrypoint(routes, { | ||
| functionsDirectory: "/project/functions", | ||
| fallbackService: "ASSETS", | ||
| }); | ||
|
|
||
| // path-to-regexp is imported from its resolved absolute path | ||
| expect(code).toMatch(/import \{ match \} from ".*path-to-regexp.*"/); | ||
| expect(code).toContain("import { onRequestGet as"); | ||
| expect(code).toContain('routePath: "/api/:id"'); | ||
| expect(code).toContain('mountPath: "/api"'); | ||
| expect(code).toContain('method: "GET"'); | ||
| expect(code).toContain( | ||
| "createPagesHandler(routes, __FALLBACK_SERVICE__)" | ||
| ); | ||
| }); | ||
|
|
||
| it("handles middleware routes", () => { | ||
| const routes: RouteConfig[] = [ | ||
| { | ||
| routePath: "/", | ||
| mountPath: "/", | ||
| middleware: ["_middleware.ts:onRequest"], | ||
| module: ["index.ts:onRequest"], | ||
| }, | ||
| ]; | ||
|
|
||
| const code = generateWorkerEntrypoint(routes, { | ||
| functionsDirectory: "/project/functions", | ||
| }); | ||
|
|
||
| expect(code).toContain("middlewares: ["); | ||
| expect(code).toContain("modules: ["); | ||
| }); | ||
|
|
||
| it("generates unique identifiers for duplicate export names", () => { | ||
| const routes: RouteConfig[] = [ | ||
| { | ||
| routePath: "/a", | ||
| mountPath: "/a", | ||
| module: ["a.ts:onRequest"], | ||
| }, | ||
| { | ||
| routePath: "/b", | ||
| mountPath: "/b", | ||
| module: ["b.ts:onRequest"], | ||
| }, | ||
| ]; | ||
|
|
||
| const code = generateWorkerEntrypoint(routes, { | ||
| functionsDirectory: "/project/functions", | ||
| }); | ||
|
|
||
| // Should have two different identifiers | ||
| const matches = code.match(/import \{ onRequest as (\w+) \}/g); | ||
| expect(matches).toHaveLength(2); | ||
| }); | ||
|
|
||
| it("includes runtime code", () => { | ||
| const routes: RouteConfig[] = [ | ||
| { | ||
| routePath: "/", | ||
| mountPath: "/", | ||
| module: ["index.ts:onRequest"], | ||
| }, | ||
| ]; | ||
|
|
||
| const code = generateWorkerEntrypoint(routes, { | ||
| functionsDirectory: "/project/functions", | ||
| }); | ||
|
|
||
| // Runtime code should be inlined | ||
| expect(code).toContain("function* executeRequest"); | ||
| expect(code).toContain("function createPagesHandler"); | ||
| expect(code).toContain("cloneResponse"); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.