Skip to content

Commit

Permalink
feat: custom worker entrypoint (#828)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-elicx authored Jul 28, 2024
1 parent f2e7bc0 commit 78accfd
Show file tree
Hide file tree
Showing 16 changed files with 184 additions and 36 deletions.
23 changes: 23 additions & 0 deletions .changeset/silly-coins-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@cloudflare/next-on-pages': minor
---

Add support for custom worker entrypoints.

Example:

```ts
import nextOnPagesHandler from '@cloudflare/next-on-pages/fetch-handler';

export default {
async fetch(request, env, ctx) {
// do something before running the next-on-pages handler

const response = await nextOnPagesHandler.fetch(request, env, ctx);

// do something after running the next-on-pages handler

return response;
},
} as ExportedHandler<{ ASSETS: Fetcher }>;
```
34 changes: 34 additions & 0 deletions packages/next-on-pages/docs/advanced-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Advanced Usage

## Custom Worker Entrypoint

Certain use cases may require the ability the control what happens in your Pages project's worker. Observability requirements, for instance, might benefit from being able to intercept console logs, catch uncaught exceptions, or monitor the time spent doing work in the next-on-pages router.

All of these would require modifying the worker to add some code before and/or after next-on-pages' logic runs.

To achieve this, next-on-pages exposes an option to use your own worker entrypoint. Within it, you can directly import and use the next-on-pages fetch handler.

1. Create a handler in your project.

```ts
// file: ./custom-entrypoint.ts
import nextOnPagesHandler from '@cloudflare/next-on-pages/fetch-handler';

export default {
async fetch(request, env, ctx) {
// do something before running the next-on-pages handler

const response = await nextOnPagesHandler.fetch(request, env, ctx);

// do something after running the next-on-pages handler

return response;
},
} as ExportedHandler<{ ASSETS: Fetcher }>;
```

2. Pass the entrypoint argument to the next-on-pages CLI with the path to your handler.

```sh
npx @cloudflare/next-on-pages --custom-entrypoint=./custom-entrypoint.ts
```
9 changes: 7 additions & 2 deletions packages/next-on-pages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"import": "./dist/api/index.js",
"types": "./dist/api/index.d.ts"
},
"./fetch-handler": {
"import": "./dist/fetch-handler/index.js",
"types": "./dist/fetch-handler/index.d.ts"
},
"./next-dev": {
"import": "./dist/next-dev/index.cjs",
"require": "./dist/next-dev/index.cjs",
Expand All @@ -16,12 +20,13 @@
"scripts": {
"lint": "eslint src templates",
"types-check": "tsc --noEmit",
"build:types": "tsc -p tsconfig.api.json",
"build:types": "tsc -p tsconfig.api.json -p tsconfig.fetch-handler.json",
"build": "esbuild --bundle --platform=node ./src/index.ts ./src/api/index.ts --external:esbuild --external:chokidar --external:server-only --outdir=./dist",
"build:watch": "npm run build -- --watch=forever",
"build:no-nodejs-compat-error-page": "node ./build-no-nodejs-compat-flag-static-error-page.mjs",
"build:next-dev": "npm run build --workspace @cloudflare/next-on-pages-next-dev && rm -rf ./dist/next-dev && cp -R ../../internal-packages/next-dev/dist ./dist/next-dev",
"postbuild": "npm run build:types && npm run build:no-nodejs-compat-error-page && npm run build:next-dev",
"build:fetch-handler": "esbuild --bundle --platform=browser ./src/fetch-handler/index.ts --external:server-only --outdir=./dist/fetch-handler",
"postbuild": "npm run build:types && npm run build:no-nodejs-compat-error-page && npm run build:next-dev && npm run build:fetch-handler",
"prepare": "npm run build",
"test:unit": "npx vitest --config vitest.config.ts"
},
Expand Down
22 changes: 16 additions & 6 deletions packages/next-on-pages/src/buildApplication/buildApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export async function buildApplication({
disableWorkerMinification,
watch,
outdir: outputDir,
customEntrypoint,
}: Pick<
CliOptions,
| 'skipBuild'
| 'disableChunksDedup'
| 'disableWorkerMinification'
| 'watch'
| 'outdir'
| 'customEntrypoint'
>) {
const pm = await getPackageManager();

Expand Down Expand Up @@ -84,6 +86,7 @@ export async function buildApplication({
await prepareAndBuildWorker(outputDir, {
disableChunksDedup,
disableWorkerMinification,
customEntrypoint,
});

const totalBuildTime = ((Date.now() - buildStartTime) / 1000).toFixed(2);
Expand All @@ -95,7 +98,11 @@ async function prepareAndBuildWorker(
{
disableChunksDedup,
disableWorkerMinification,
}: Pick<CliOptions, 'disableChunksDedup' | 'disableWorkerMinification'>,
customEntrypoint,
}: Pick<
CliOptions,
'disableChunksDedup' | 'disableWorkerMinification' | 'customEntrypoint'
>,
): Promise<void> {
let vercelConfig: VercelConfig;
try {
Expand Down Expand Up @@ -140,11 +147,14 @@ async function prepareAndBuildWorker(
processedFunctions?.collectedFunctions?.edgeFunctions,
);

const outputtedWorkerPath = await buildWorkerFile(
processedVercelOutput,
{ outputDir, workerJsDir, nopDistDir, templatesDir },
!disableWorkerMinification,
);
const outputtedWorkerPath = await buildWorkerFile(processedVercelOutput, {
outputDir,
workerJsDir,
nopDistDir,
templatesDir,
customEntrypoint,
minify: !disableWorkerMinification,
});

await buildMetadataFiles(outputDir, { staticAssets });

Expand Down
56 changes: 44 additions & 12 deletions packages/next-on-pages/src/buildApplication/buildWorkerFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { generateGlobalJs } from './generateGlobalJs';
import type { ProcessedVercelOutput } from './processVercelOutput';
import { getNodeEnv } from '../utils/getNodeEnv';
import { normalizePath } from '../utils';
import { cliLog } from '../cli';

/**
* Construct a record for the build output map.
Expand Down Expand Up @@ -41,8 +42,14 @@ export function constructBuildOutputRecord(

export async function buildWorkerFile(
{ vercelConfig, vercelOutput }: ProcessedVercelOutput,
{ outputDir, workerJsDir, nopDistDir, templatesDir }: BuildWorkerFileOpts,
minify: boolean,
{
outputDir,
workerJsDir,
nopDistDir,
templatesDir,
customEntrypoint,
minify,
}: BuildWorkerFileOpts,
): Promise<string> {
const functionsFile = join(
tmpdir(),
Expand All @@ -59,17 +66,21 @@ export async function buildWorkerFile(
.join(',')}};`,
);

const defaultBuildOpts = {
target: 'es2022',
platform: 'neutral',
bundle: false,
minify,
} as const;

const outputFile = join(workerJsDir, 'index.js');

await build({
...defaultBuildOpts,
entryPoints: [join(templatesDir, '_worker.js')],
banner: {
js: generateGlobalJs(),
},
banner: { js: generateGlobalJs() },
bundle: true,
inject: [functionsFile],
target: 'es2022',
platform: 'neutral',
external: ['node:*', './__next-on-pages-dist__/*', 'cloudflare:*'],
define: {
__CONFIG__: JSON.stringify(vercelConfig),
Expand All @@ -79,20 +90,39 @@ export async function buildWorkerFile(
}),
},
outfile: outputFile,
minify,
});

await build({
...defaultBuildOpts,
entryPoints: ['adaptor.ts', 'cache-api.ts', 'kv.ts'].map(fileName =>
join(templatesDir, 'cache', fileName),
),
bundle: false,
target: 'es2022',
platform: 'neutral',
outdir: join(nopDistDir, 'cache'),
minify,
});

if (customEntrypoint) {
cliLog(`Using custom worker entrypoint '${customEntrypoint}'`);

await build({
...defaultBuildOpts,
entryPoints: [customEntrypoint],
outfile: outputFile,
allowOverwrite: true,
bundle: true,
plugins: [
{
name: 'custom-entrypoint-import-plugin',
setup(build) {
build.onResolve(
{ filter: /^@cloudflare\/next-on-pages\/fetch-handler$/ },
() => ({ path: outputFile }),
);
},
},
],
});
}

return relative('.', outputFile);
}

Expand All @@ -101,6 +131,8 @@ type BuildWorkerFileOpts = {
workerJsDir: string;
nopDistDir: string;
templatesDir: string;
customEntrypoint?: string;
minify?: boolean;
};

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/next-on-pages/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ program
'Sets the directory to output the worker and static assets to',
join('.vercel', 'output', 'static'),
)
.option(
'--custom-entrypoint <path>',
'Wrap the generated worker for your application in a custom worker entrypoint',
)
.enablePositionalOptions(false)
.version(
nextOnPagesVersion,
Expand All @@ -74,6 +78,7 @@ export type CliOptions = {
noColor?: boolean;
info?: boolean;
outdir: string;
customEntrypoint?: string;
};

export function parseCliArgs(): CliOptions {
Expand Down
9 changes: 9 additions & 0 deletions packages/next-on-pages/src/fetch-handler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'server-only';

export default {
async fetch() {
throw new Error(
'Invalid invocation of the next-on-pages fetch handler - this method should only be used alongside the --custom-entrypoint CLI option. For more details, see: https://github.com/cloudflare/next-on-pages/blob/main/packages/next-on-pages/docs/advanced-usage.md#custom-entrypoint',
);
},
} as { fetch: ExportedHandlerFetchHandler<{ ASSETS: Fetcher }> };
10 changes: 10 additions & 0 deletions packages/next-on-pages/tsconfig.fetch-handler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "@cloudflare/next-on-pages-tsconfig/tsconfig.json",
"include": ["src/fetch-handler"],
"compilerOptions": {
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist/fetch-handler"
}
}
11 changes: 11 additions & 0 deletions pages-e2e/features/customEntrypoint/assets/custom-entrypoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import nextOnPagesHandler from '@cloudflare/next-on-pages/fetch-handler';

export default {
async fetch(...args) {
const response = await nextOnPagesHandler.fetch(...args);

response.headers.set('custom-entrypoint', '1');

return response;
},
} as ExportedHandler<{ ASSETS: Fetcher }>;
12 changes: 12 additions & 0 deletions pages-e2e/features/customEntrypoint/custom-entrypoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, test } from 'vitest';

describe('Custom Entrypoint', () => {
test('should set header on response in the worker entrypoint', async ({
expect,
}) => {
const response = await fetch(`${DEPLOYMENT_URL}/api/hello`);

await expect(response.text()).resolves.toEqual('Hello world');
expect(response.headers.get('custom-entrypoint')).toEqual('1');
});
});
3 changes: 3 additions & 0 deletions pages-e2e/features/customEntrypoint/main.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"setup": "node --loader tsm setup.ts"
}
2 changes: 2 additions & 0 deletions pages-e2e/features/customEntrypoint/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { copyWorkspaceAssets } from '../_utils/copyWorkspaceAssets';
await copyWorkspaceAssets();
5 changes: 1 addition & 4 deletions pages-e2e/fixtures/app13.4.0/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import './globals.css';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
title: 'Create Next App',
Expand All @@ -15,7 +12,7 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body>{children}</body>
</html>
);
}
5 changes: 1 addition & 4 deletions pages-e2e/fixtures/app14.0.0/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
Expand All @@ -16,7 +13,7 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body>{children}</body>
</html>
);
}
9 changes: 5 additions & 4 deletions pages-e2e/fixtures/appLatest/main.fixture
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@
"appConfigsRewritesRedirectsHeaders",
"appWasm",
"appServerActions",
"appGetRequestContext"
"appGetRequestContext",
"customEntrypoint"
],
"localSetup": "./setup.sh",
"buildConfig": {
"buildCommand": "npx --force ../../../packages/next-on-pages",
"buildCommand": "npx --force ../../../packages/next-on-pages --custom-entrypoint=./custom-entrypoint.ts",
"buildOutputDirectory": ".vercel/output/static"
},
"deploymentConfig": {
"compatibilityFlags": ["nodejs_compat"],
"kvNamespaces": {
"MY_KV": {
"production": {"id": "00000000000000000000000000000000"},
"staging": {"id": "00000000000000000000000000000000"}
"production": { "id": "00000000000000000000000000000000" },
"staging": { "id": "00000000000000000000000000000000" }
}
}
}
Expand Down
Loading

0 comments on commit 78accfd

Please sign in to comment.