@opennextjs/cloudflare 0.3.10 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -64
- package/dist/api/{get-cloudflare-context.d.ts → cloudflare-context.d.ts} +8 -1
- package/dist/api/cloudflare-context.js +96 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/kvCache.d.ts +0 -5
- package/dist/api/kvCache.js +15 -28
- package/dist/cli/build/build.d.ts +1 -1
- package/dist/cli/build/build.js +2 -12
- package/dist/cli/build/bundle-server.d.ts +4 -5
- package/dist/cli/build/bundle-server.js +32 -36
- package/dist/cli/build/open-next/compile-env-files.d.ts +1 -1
- package/dist/cli/build/open-next/compile-env-files.js +2 -2
- package/dist/cli/build/patches/ast/optional-deps.d.ts +4 -1
- package/dist/cli/build/patches/ast/optional-deps.js +2 -2
- package/dist/cli/build/patches/ast/util.d.ts +15 -4
- package/dist/cli/build/patches/ast/util.js +16 -5
- package/dist/cli/build/patches/ast/vercel-og.d.ts +23 -0
- package/dist/cli/build/patches/ast/vercel-og.js +58 -0
- package/dist/cli/build/patches/ast/vercel-og.spec.d.ts +1 -0
- package/dist/cli/build/patches/ast/vercel-og.spec.js +22 -0
- package/dist/cli/build/patches/investigated/copy-package-cli-files.d.ts +4 -3
- package/dist/cli/build/patches/investigated/copy-package-cli-files.js +8 -5
- package/dist/cli/build/patches/investigated/index.d.ts +1 -0
- package/dist/cli/build/patches/investigated/index.js +1 -0
- package/dist/cli/build/patches/investigated/patch-cache.d.ts +2 -2
- package/dist/cli/build/patches/investigated/patch-cache.js +7 -6
- package/dist/cli/build/patches/investigated/patch-require.d.ts +1 -4
- package/dist/cli/build/patches/investigated/patch-require.js +1 -4
- package/dist/cli/build/patches/investigated/patch-vercel-og-library.d.ts +7 -0
- package/dist/cli/build/patches/investigated/patch-vercel-og-library.js +39 -0
- package/dist/cli/build/patches/investigated/patch-vercel-og-library.spec.d.ts +1 -0
- package/dist/cli/build/patches/investigated/patch-vercel-og-library.spec.js +50 -0
- package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.d.ts +2 -5
- package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.js +6 -6
- package/dist/cli/build/patches/to-investigate/inline-eval-manifest.d.ts +2 -2
- package/dist/cli/build/patches/to-investigate/inline-eval-manifest.js +21 -17
- package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.d.ts +2 -2
- package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.js +4 -2
- package/dist/cli/build/patches/to-investigate/inline-next-require.d.ts +2 -2
- package/dist/cli/build/patches/to-investigate/inline-next-require.js +16 -12
- package/dist/cli/build/patches/to-investigate/patch-find-dir.d.ts +3 -6
- package/dist/cli/build/patches/to-investigate/patch-find-dir.js +11 -11
- package/dist/cli/build/patches/to-investigate/patch-read-file.d.ts +3 -3
- package/dist/cli/build/patches/to-investigate/patch-read-file.js +15 -16
- package/dist/cli/build/patches/to-investigate/wrangler-deps.d.ts +2 -2
- package/dist/cli/build/patches/to-investigate/wrangler-deps.js +36 -67
- package/dist/cli/build/utils/create-config-files.d.ts +1 -1
- package/dist/cli/project-options.d.ts +7 -0
- package/dist/cli/project-options.js +1 -0
- package/package.json +4 -4
- package/dist/api/get-cloudflare-context.js +0 -42
- package/dist/cli/config.d.ts +0 -42
- package/dist/cli/config.js +0 -92
package/README.md
CHANGED
|
@@ -6,70 +6,7 @@ Deploy Next.js apps to Cloudflare!
|
|
|
6
6
|
|
|
7
7
|
## Get started
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Configure your app
|
|
12
|
-
|
|
13
|
-
- add the following `devDependencies` to the `package.json`:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm add -D wrangler@latest @opennextjs/cloudflare
|
|
17
|
-
# or
|
|
18
|
-
pnpm add -D wrangler@latest @opennextjs/cloudflare
|
|
19
|
-
# or
|
|
20
|
-
yarn add -D wrangler@latest @opennextjs/cloudflare
|
|
21
|
-
# or
|
|
22
|
-
bun add -D wrangler@latest @opennextjs/cloudflare
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
- add a `wrangler.toml` at the root of your project
|
|
26
|
-
|
|
27
|
-
```toml
|
|
28
|
-
#:schema node_modules/wrangler/config-schema.json
|
|
29
|
-
name = "<your-app-name>"
|
|
30
|
-
main = ".open-next/worker.js"
|
|
31
|
-
|
|
32
|
-
compatibility_date = "2024-09-23"
|
|
33
|
-
compatibility_flags = ["nodejs_compat"]
|
|
34
|
-
|
|
35
|
-
# Use the new Workers + Assets to host the static frontend files
|
|
36
|
-
assets = { directory = ".open-next/assets", binding = "ASSETS" }
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
- add a `open-next.config.ts` at the root of your project:
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import type { OpenNextConfig } from "open-next/types/open-next";
|
|
43
|
-
|
|
44
|
-
const config: OpenNextConfig = {
|
|
45
|
-
default: {
|
|
46
|
-
override: {
|
|
47
|
-
wrapper: "cloudflare-node",
|
|
48
|
-
converter: "edge",
|
|
49
|
-
// Unused implementation
|
|
50
|
-
incrementalCache: "dummy",
|
|
51
|
-
tagCache: "dummy",
|
|
52
|
-
queue: "dummy",
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
middleware: {
|
|
57
|
-
external: true,
|
|
58
|
-
override: {
|
|
59
|
-
wrapper: "cloudflare-edge",
|
|
60
|
-
converter: "edge",
|
|
61
|
-
proxyExternalRequest: "fetch",
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export default config;
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Known issues
|
|
70
|
-
|
|
71
|
-
- `▲ [WARNING] Suspicious assignment to defined constant "process.env.NODE_ENV" [assign-to-define]` can safely be ignored
|
|
72
|
-
- Maybe more, still experimental...
|
|
9
|
+
To get started with the adapter visit the [official get started documentation](https://opennext.js.org/cloudflare/get-started).
|
|
73
10
|
|
|
74
11
|
## Local development
|
|
75
12
|
|
|
@@ -23,4 +23,11 @@ export type CloudflareContext<CfProperties extends Record<string, unknown> = Inc
|
|
|
23
23
|
*
|
|
24
24
|
* @returns the cloudflare context
|
|
25
25
|
*/
|
|
26
|
-
export declare function getCloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext>():
|
|
26
|
+
export declare function getCloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext>(): CloudflareContext<CfProperties, Context>;
|
|
27
|
+
/**
|
|
28
|
+
* Performs some initial setup to integrate as best as possible the local Next.js dev server (run via `next dev`)
|
|
29
|
+
* with the open-next Cloudflare adapter
|
|
30
|
+
*
|
|
31
|
+
* Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
|
|
32
|
+
*/
|
|
33
|
+
export declare function initOpenNextCloudflareForDev(): Promise<void>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol used as an index in the global scope to set and retrieve the Cloudflare context
|
|
3
|
+
*
|
|
4
|
+
* This is used both in production (in the actual built worker) and in development (`next dev`)
|
|
5
|
+
*
|
|
6
|
+
* Note: this symbol needs to be kept in sync with the one used in `src/cli/templates/worker.ts`
|
|
7
|
+
*/
|
|
8
|
+
const cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
|
|
9
|
+
/**
|
|
10
|
+
* Utility to get the current Cloudflare context
|
|
11
|
+
*
|
|
12
|
+
* @returns the cloudflare context
|
|
13
|
+
*/
|
|
14
|
+
export function getCloudflareContext() {
|
|
15
|
+
const global = globalThis;
|
|
16
|
+
const cloudflareContext = global[cloudflareContextSymbol];
|
|
17
|
+
if (!cloudflareContext) {
|
|
18
|
+
// the cloudflare context is initialized by the worker and is always present in production/preview
|
|
19
|
+
// during local development (`next dev`) it might be missing only if the developers hasn't called
|
|
20
|
+
// the `initOpenNextCloudflareForDev` function in their Next.js config file
|
|
21
|
+
const getContextFunctionName = getCloudflareContext.name;
|
|
22
|
+
const initFunctionName = initOpenNextCloudflareForDev.name;
|
|
23
|
+
throw new Error(`\n\n\`${getContextFunctionName}\` has been called during development without having called` +
|
|
24
|
+
` the \`${initFunctionName}\` function inside the Next.js config file.\n\n` +
|
|
25
|
+
`In order to use \`${getContextFunctionName}\` import and call ${initFunctionName} in the Next.js config file.\n\n` +
|
|
26
|
+
"Example: \n ```\n // next.config.mjs\n\n" +
|
|
27
|
+
` import { ${initFunctionName} } from "@opennextjs/cloudflare";\n\n` +
|
|
28
|
+
` ${initFunctionName}();\n\n` +
|
|
29
|
+
" /** @type {import('next').NextConfig} */\n" +
|
|
30
|
+
" const nextConfig = {};\n" +
|
|
31
|
+
" export default nextConfig;\n" +
|
|
32
|
+
" ```\n" +
|
|
33
|
+
"\n(note: currently middlewares in Next.js are always run using the edge runtime)\n\n");
|
|
34
|
+
}
|
|
35
|
+
return cloudflareContext;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Performs some initial setup to integrate as best as possible the local Next.js dev server (run via `next dev`)
|
|
39
|
+
* with the open-next Cloudflare adapter
|
|
40
|
+
*
|
|
41
|
+
* Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
|
|
42
|
+
*/
|
|
43
|
+
export async function initOpenNextCloudflareForDev() {
|
|
44
|
+
const context = await getCloudflareContextFromWrangler();
|
|
45
|
+
addCloudflareContextToNodejsGlobal(context);
|
|
46
|
+
await monkeyPatchVmModuleEdgeContext(context);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Adds the cloudflare context to the global scope in which the Next.js dev node.js process runs in, enabling
|
|
50
|
+
* future calls to `getCloudflareContext` to retrieve and return such context
|
|
51
|
+
*
|
|
52
|
+
* @param cloudflareContext the cloudflare context to add to the node.sj global scope
|
|
53
|
+
*/
|
|
54
|
+
function addCloudflareContextToNodejsGlobal(cloudflareContext) {
|
|
55
|
+
const global = globalThis;
|
|
56
|
+
global[cloudflareContextSymbol] = cloudflareContext;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Next.js uses the Node.js vm module's `runInContext()` function to evaluate edge functions
|
|
60
|
+
* in a runtime context that tries to simulate as accurately as possible the actual production runtime
|
|
61
|
+
* behavior, see: https://github.com/vercel/next.js/blob/9a1cd3/packages/next/src/server/web/sandbox/context.ts#L525-L527
|
|
62
|
+
*
|
|
63
|
+
* This function monkey-patches the Node.js `vm` module to override the `runInContext()` function so that the
|
|
64
|
+
* cloudflare context is added to the runtime context's global scope before edge functions are evaluated
|
|
65
|
+
*
|
|
66
|
+
* @param cloudflareContext the cloudflare context to patch onto the "edge" runtime context global scope
|
|
67
|
+
*/
|
|
68
|
+
async function monkeyPatchVmModuleEdgeContext(cloudflareContext) {
|
|
69
|
+
const require = (await import(/* webpackIgnore: true */ `${"__module".replaceAll("_", "")}`)).default.createRequire(import.meta.url);
|
|
70
|
+
// eslint-disable-next-line unicorn/prefer-node-protocol -- the `next dev` compiler doesn't accept the node prefix
|
|
71
|
+
const vmModule = require("vm");
|
|
72
|
+
const originalRunInContext = vmModule.runInContext.bind(vmModule);
|
|
73
|
+
vmModule.runInContext = (code, contextifiedObject, options) => {
|
|
74
|
+
const runtimeContext = contextifiedObject;
|
|
75
|
+
runtimeContext[cloudflareContextSymbol] ??= cloudflareContext;
|
|
76
|
+
return originalRunInContext(code, contextifiedObject, options);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Gets a cloudflare context object from wrangler
|
|
81
|
+
*
|
|
82
|
+
* @returns the cloudflare context ready for use
|
|
83
|
+
*/
|
|
84
|
+
async function getCloudflareContextFromWrangler() {
|
|
85
|
+
// Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
|
|
86
|
+
const { getPlatformProxy } = await import(/* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`);
|
|
87
|
+
const { env, cf, ctx } = await getPlatformProxy({
|
|
88
|
+
// This allows the selection of a wrangler environment while running in next dev mode
|
|
89
|
+
environment: process.env.NEXT_DEV_WRANGLER_ENV,
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
env,
|
|
93
|
+
cf: cf,
|
|
94
|
+
ctx: ctx,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./cloudflare-context.js";
|
package/dist/api/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./cloudflare-context.js";
|
package/dist/api/kvCache.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { KVNamespace } from "@cloudflare/workers-types";
|
|
2
1
|
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
|
|
3
2
|
export declare const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
|
|
4
3
|
export declare const STATUS_DELETED = 1;
|
|
@@ -11,9 +10,6 @@ export declare const STATUS_DELETED = 1;
|
|
|
11
10
|
*/
|
|
12
11
|
declare class Cache implements IncrementalCache {
|
|
13
12
|
readonly name = "cloudflare-kv";
|
|
14
|
-
protected initialized: boolean;
|
|
15
|
-
protected kv: KVNamespace | undefined;
|
|
16
|
-
protected assets: Fetcher | undefined;
|
|
17
13
|
get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>>>;
|
|
18
14
|
set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
|
|
19
15
|
delete(key: string): Promise<void>;
|
|
@@ -21,7 +17,6 @@ declare class Cache implements IncrementalCache {
|
|
|
21
17
|
protected getAssetUrl(key: string, isFetch?: boolean): string;
|
|
22
18
|
protected debug(...args: unknown[]): void;
|
|
23
19
|
protected getBuildId(): string;
|
|
24
|
-
protected init(): Promise<void>;
|
|
25
20
|
}
|
|
26
21
|
declare const _default: Cache;
|
|
27
22
|
export default _default;
|
package/dist/api/kvCache.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
|
|
2
|
-
import { getCloudflareContext } from "./
|
|
2
|
+
import { getCloudflareContext } from "./cloudflare-context.js";
|
|
3
3
|
export const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
|
|
4
4
|
export const STATUS_DELETED = 1;
|
|
5
5
|
/**
|
|
@@ -11,31 +11,28 @@ export const STATUS_DELETED = 1;
|
|
|
11
11
|
*/
|
|
12
12
|
class Cache {
|
|
13
13
|
name = "cloudflare-kv";
|
|
14
|
-
initialized = false;
|
|
15
|
-
kv;
|
|
16
|
-
assets;
|
|
17
14
|
async get(key, isFetch) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!(
|
|
15
|
+
const cfEnv = getCloudflareContext().env;
|
|
16
|
+
const kv = cfEnv.NEXT_CACHE_WORKERS_KV;
|
|
17
|
+
const assets = cfEnv.ASSETS;
|
|
18
|
+
if (!(kv || assets)) {
|
|
22
19
|
throw new IgnorableError(`No KVNamespace nor Fetcher`);
|
|
23
20
|
}
|
|
24
21
|
this.debug(`Get ${key}`);
|
|
25
22
|
try {
|
|
26
23
|
let entry = null;
|
|
27
|
-
if (
|
|
24
|
+
if (kv) {
|
|
28
25
|
this.debug(`- From KV`);
|
|
29
26
|
const kvKey = this.getKVKey(key, isFetch);
|
|
30
|
-
entry = await
|
|
27
|
+
entry = await kv.get(kvKey, "json");
|
|
31
28
|
if (entry?.status === STATUS_DELETED) {
|
|
32
29
|
return {};
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
|
-
if (!entry &&
|
|
32
|
+
if (!entry && assets) {
|
|
36
33
|
this.debug(`- From Assets`);
|
|
37
34
|
const url = this.getAssetUrl(key, isFetch);
|
|
38
|
-
const response = await
|
|
35
|
+
const response = await assets.fetch(url);
|
|
39
36
|
if (response.ok) {
|
|
40
37
|
// TODO: consider populating KV with the asset value if faster.
|
|
41
38
|
// This could be optional as KV writes are $$.
|
|
@@ -55,10 +52,8 @@ class Cache {
|
|
|
55
52
|
}
|
|
56
53
|
}
|
|
57
54
|
async set(key, value, isFetch) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
if (!this.kv) {
|
|
55
|
+
const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
|
|
56
|
+
if (!kv) {
|
|
62
57
|
throw new IgnorableError(`No KVNamespace`);
|
|
63
58
|
}
|
|
64
59
|
this.debug(`Set ${key}`);
|
|
@@ -67,7 +62,7 @@ class Cache {
|
|
|
67
62
|
// Note: We can not set a TTL as we might fallback to assets,
|
|
68
63
|
// still removing old data (old BUILD_ID) could help avoiding
|
|
69
64
|
// the cache growing too big.
|
|
70
|
-
await
|
|
65
|
+
await kv.put(kvKey, JSON.stringify({
|
|
71
66
|
value,
|
|
72
67
|
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
|
73
68
|
// See https://developers.cloudflare.com/workers/reference/security-model/
|
|
@@ -79,17 +74,15 @@ class Cache {
|
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
76
|
async delete(key) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (!this.kv) {
|
|
77
|
+
const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
|
|
78
|
+
if (!kv) {
|
|
86
79
|
throw new IgnorableError(`No KVNamespace`);
|
|
87
80
|
}
|
|
88
81
|
this.debug(`Delete ${key}`);
|
|
89
82
|
try {
|
|
90
83
|
const kvKey = this.getKVKey(key, /* isFetch= */ false);
|
|
91
84
|
// Do not delete the key as we would then fallback to the assets.
|
|
92
|
-
await
|
|
85
|
+
await kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
|
|
93
86
|
}
|
|
94
87
|
catch {
|
|
95
88
|
throw new RecoverableError(`Failed to delete cache [${key}]`);
|
|
@@ -111,11 +104,5 @@ class Cache {
|
|
|
111
104
|
getBuildId() {
|
|
112
105
|
return process.env.NEXT_BUILD_ID ?? "no-build-id";
|
|
113
106
|
}
|
|
114
|
-
async init() {
|
|
115
|
-
const env = (await getCloudflareContext()).env;
|
|
116
|
-
this.kv = env.NEXT_CACHE_WORKERS_KV;
|
|
117
|
-
this.assets = env.ASSETS;
|
|
118
|
-
this.initialized = true;
|
|
119
|
-
}
|
|
120
107
|
}
|
|
121
108
|
export default new Cache();
|
package/dist/cli/build/build.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { cpSync } from "node:fs";
|
|
2
1
|
import { createRequire } from "node:module";
|
|
3
|
-
import { dirname
|
|
2
|
+
import { dirname } from "node:path";
|
|
4
3
|
import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js";
|
|
5
4
|
import { compileCache } from "@opennextjs/aws/build/compileCache.js";
|
|
6
5
|
import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
|
|
@@ -9,7 +8,6 @@ import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js";
|
|
|
9
8
|
import * as buildHelper from "@opennextjs/aws/build/helper.js";
|
|
10
9
|
import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
|
|
11
10
|
import logger from "@opennextjs/aws/logger.js";
|
|
12
|
-
import { containsDotNextDir, getConfig } from "../config.js";
|
|
13
11
|
import { bundleServer } from "./bundle-server.js";
|
|
14
12
|
import { compileEnvFiles } from "./open-next/compile-env-files.js";
|
|
15
13
|
import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
|
|
@@ -52,9 +50,6 @@ export async function build(projectOpts) {
|
|
|
52
50
|
setStandaloneBuildMode(options);
|
|
53
51
|
buildNextjsApp(options);
|
|
54
52
|
}
|
|
55
|
-
if (!containsDotNextDir(projectOpts.sourceDir)) {
|
|
56
|
-
throw new Error(`.next folder not found in ${projectOpts.sourceDir}`);
|
|
57
|
-
}
|
|
58
53
|
// Generate deployable bundle
|
|
59
54
|
printHeader("Generating bundle");
|
|
60
55
|
buildHelper.initOutputDir(options);
|
|
@@ -70,12 +65,7 @@ export async function build(projectOpts) {
|
|
|
70
65
|
copyCacheAssets(options);
|
|
71
66
|
}
|
|
72
67
|
await createServerBundle(options);
|
|
73
|
-
|
|
74
|
-
// Copy the .next directory to the output directory so it can be mutated.
|
|
75
|
-
cpSync(join(projectOpts.sourceDir, ".next"), join(projectOpts.outputDir, ".next"), { recursive: true });
|
|
76
|
-
const projConfig = getConfig(projectOpts);
|
|
77
|
-
// TODO: rely on options only.
|
|
78
|
-
await bundleServer(projConfig, options);
|
|
68
|
+
await bundleServer(options);
|
|
79
69
|
if (!projectOpts.skipWranglerConfigCheck) {
|
|
80
70
|
await createWranglerConfigIfNotExistent(projectOpts);
|
|
81
71
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { Config } from "../config.js";
|
|
1
|
+
import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
|
|
3
2
|
/**
|
|
4
3
|
* Bundle the Open Next server.
|
|
5
4
|
*/
|
|
6
|
-
export declare function bundleServer(
|
|
5
|
+
export declare function bundleServer(buildOpts: BuildOptions): Promise<void>;
|
|
7
6
|
/**
|
|
8
7
|
* Gets the path of the worker.js file generated by the build process
|
|
9
8
|
*
|
|
10
|
-
* @param
|
|
9
|
+
* @param buildOpts the open-next build options
|
|
11
10
|
* @returns the path of the worker.js file that the build process generates
|
|
12
11
|
*/
|
|
13
|
-
export declare function getOutputWorkerPath(
|
|
12
|
+
export declare function getOutputWorkerPath(buildOpts: BuildOptions): string;
|
|
@@ -3,6 +3,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Lang, parse } from "@ast-grep/napi";
|
|
6
|
+
import { getPackagePath } from "@opennextjs/aws/build/helper.js";
|
|
6
7
|
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
|
|
7
8
|
import { build } from "esbuild";
|
|
8
9
|
import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
|
|
@@ -13,17 +14,17 @@ const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "
|
|
|
13
14
|
/**
|
|
14
15
|
* Bundle the Open Next server.
|
|
15
16
|
*/
|
|
16
|
-
export async function bundleServer(
|
|
17
|
-
patches.copyPackageCliFiles(packageDistDir,
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
export async function bundleServer(buildOpts) {
|
|
18
|
+
patches.copyPackageCliFiles(packageDistDir, buildOpts);
|
|
19
|
+
const { appPath, outputDir, monorepoRoot } = buildOpts;
|
|
20
|
+
const serverFiles = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/required-server-files.json");
|
|
21
|
+
const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
|
|
21
22
|
console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
|
|
22
|
-
patches.patchWranglerDeps(
|
|
23
|
-
patches.updateWebpackChunksFile(
|
|
24
|
-
|
|
23
|
+
patches.patchWranglerDeps(buildOpts);
|
|
24
|
+
await patches.updateWebpackChunksFile(buildOpts);
|
|
25
|
+
patches.patchVercelOgLibrary(buildOpts);
|
|
25
26
|
const outputPath = path.join(outputDir, "server-functions", "default");
|
|
26
|
-
const packagePath =
|
|
27
|
+
const packagePath = getPackagePath(buildOpts);
|
|
27
28
|
const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
|
|
28
29
|
const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
|
|
29
30
|
await build({
|
|
@@ -33,25 +34,25 @@ export async function bundleServer(config, openNextOptions) {
|
|
|
33
34
|
format: "esm",
|
|
34
35
|
target: "esnext",
|
|
35
36
|
minify: false,
|
|
36
|
-
plugins: [createFixRequiresESBuildPlugin(
|
|
37
|
+
plugins: [createFixRequiresESBuildPlugin(buildOpts)],
|
|
37
38
|
external: ["./middleware/handler.mjs", "caniuse-lite"],
|
|
38
39
|
alias: {
|
|
39
40
|
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
|
|
40
41
|
// eval("require")("bufferutil");
|
|
41
42
|
// eval("require")("utf-8-validate");
|
|
42
|
-
"next/dist/compiled/ws": path.join(
|
|
43
|
+
"next/dist/compiled/ws": path.join(buildOpts.outputDir, "cloudflare-templates/shims/empty.js"),
|
|
43
44
|
// Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`:
|
|
44
45
|
// eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext));
|
|
45
46
|
// which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63
|
|
46
47
|
// QUESTION: Why did I encountered this but mhart didn't?
|
|
47
|
-
"next/dist/compiled/edge-runtime": path.join(
|
|
48
|
+
"next/dist/compiled/edge-runtime": path.join(buildOpts.outputDir, "cloudflare-templates/shims/empty.js"),
|
|
48
49
|
// `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here
|
|
49
50
|
// source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env
|
|
50
|
-
"@next/env": path.join(
|
|
51
|
+
"@next/env": path.join(buildOpts.outputDir, "cloudflare-templates/shims/env.js"),
|
|
51
52
|
},
|
|
52
53
|
define: {
|
|
53
54
|
// config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
|
|
54
|
-
"process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": JSON.stringify(
|
|
55
|
+
"process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": `${JSON.stringify(nextConfig)}`,
|
|
55
56
|
// Next.js tried to access __dirname so we need to define it
|
|
56
57
|
__dirname: '""',
|
|
57
58
|
// Note: we need the __non_webpack_require__ variable declared as it is used by next-server:
|
|
@@ -104,34 +105,29 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
|
|
|
104
105
|
`,
|
|
105
106
|
},
|
|
106
107
|
});
|
|
107
|
-
await updateWorkerBundledCode(openNextServerBundle,
|
|
108
|
+
await updateWorkerBundledCode(openNextServerBundle, buildOpts);
|
|
108
109
|
const isMonorepo = monorepoRoot !== appPath;
|
|
109
110
|
if (isMonorepo) {
|
|
110
111
|
fs.writeFileSync(path.join(outputPath, "handler.mjs"), `export * from "./${normalizePath(packagePath)}/handler.mjs";`);
|
|
111
112
|
}
|
|
112
|
-
console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(
|
|
113
|
+
console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`);
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
|
-
* This function applies
|
|
116
|
-
*
|
|
117
|
-
* Needless to say all the logic in this function is something we should avoid as much as possible!
|
|
118
|
-
*
|
|
119
|
-
* @param workerOutputFile
|
|
120
|
-
* @param config
|
|
116
|
+
* This function applies patches required for the code to run on workers.
|
|
121
117
|
*/
|
|
122
|
-
async function updateWorkerBundledCode(workerOutputFile,
|
|
118
|
+
async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
|
|
123
119
|
const code = await readFile(workerOutputFile, "utf8");
|
|
124
120
|
const patchedCode = await patchCodeWithValidations(code, [
|
|
125
121
|
["require", patches.patchRequire],
|
|
126
|
-
["`buildId` function", (code) => patches.patchBuildId(code,
|
|
127
|
-
["`loadManifest` function", (code) => patches.patchLoadManifest(code,
|
|
128
|
-
["next's require", (code) => patches.inlineNextRequire(code,
|
|
129
|
-
["`findDir` function", (code) => patches.patchFindDir(code,
|
|
130
|
-
["`evalManifest` function", (code) => patches.inlineEvalManifest(code,
|
|
131
|
-
["cacheHandler", (code) => patches.patchCache(code,
|
|
122
|
+
["`buildId` function", (code) => patches.patchBuildId(code, buildOpts)],
|
|
123
|
+
["`loadManifest` function", (code) => patches.patchLoadManifest(code, buildOpts)],
|
|
124
|
+
["next's require", (code) => patches.inlineNextRequire(code, buildOpts)],
|
|
125
|
+
["`findDir` function", (code) => patches.patchFindDir(code, buildOpts)],
|
|
126
|
+
["`evalManifest` function", (code) => patches.inlineEvalManifest(code, buildOpts)],
|
|
127
|
+
["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
|
|
132
128
|
[
|
|
133
129
|
"'require(this.middlewareManifestPath)'",
|
|
134
|
-
(code) => patches.inlineMiddlewareManifestRequire(code,
|
|
130
|
+
(code) => patches.inlineMiddlewareManifestRequire(code, buildOpts),
|
|
135
131
|
],
|
|
136
132
|
["exception bubbling", patches.patchExceptionBubbling],
|
|
137
133
|
["`loadInstrumentationModule` function", patches.patchLoadInstrumentationModule],
|
|
@@ -153,16 +149,16 @@ async function updateWorkerBundledCode(workerOutputFile, config, openNextOptions
|
|
|
153
149
|
],
|
|
154
150
|
]);
|
|
155
151
|
const bundle = parse(Lang.TypeScript, patchedCode).root();
|
|
156
|
-
const edits = patchOptionalDependencies(bundle);
|
|
152
|
+
const { edits } = patchOptionalDependencies(bundle);
|
|
157
153
|
await writeFile(workerOutputFile, bundle.commitEdits(edits));
|
|
158
154
|
}
|
|
159
|
-
function createFixRequiresESBuildPlugin(
|
|
155
|
+
function createFixRequiresESBuildPlugin(options) {
|
|
160
156
|
return {
|
|
161
157
|
name: "replaceRelative",
|
|
162
158
|
setup(build) {
|
|
163
159
|
// Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
|
|
164
160
|
build.onResolve({ filter: getCrossPlatformPathRegex(String.raw `^\./require-hook$`, { escape: false }) }, () => ({
|
|
165
|
-
path: path.join(
|
|
161
|
+
path: path.join(options.outputDir, "cloudflare-templates/shims/empty.js"),
|
|
166
162
|
}));
|
|
167
163
|
},
|
|
168
164
|
};
|
|
@@ -170,9 +166,9 @@ function createFixRequiresESBuildPlugin(config) {
|
|
|
170
166
|
/**
|
|
171
167
|
* Gets the path of the worker.js file generated by the build process
|
|
172
168
|
*
|
|
173
|
-
* @param
|
|
169
|
+
* @param buildOpts the open-next build options
|
|
174
170
|
* @returns the path of the worker.js file that the build process generates
|
|
175
171
|
*/
|
|
176
|
-
export function getOutputWorkerPath(
|
|
177
|
-
return path.join(
|
|
172
|
+
export function getOutputWorkerPath(buildOpts) {
|
|
173
|
+
return path.join(buildOpts.outputDir, "worker.js");
|
|
178
174
|
}
|
|
@@ -2,4 +2,4 @@ import { BuildOptions } from "@opennextjs/aws/build/helper.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Compiles the values extracted from the project's env files to the output directory for use in the worker.
|
|
4
4
|
*/
|
|
5
|
-
export declare function compileEnvFiles(
|
|
5
|
+
export declare function compileEnvFiles(buildOpts: BuildOptions): void;
|
|
@@ -4,6 +4,6 @@ import { extractProjectEnvVars } from "../utils/index.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Compiles the values extracted from the project's env files to the output directory for use in the worker.
|
|
6
6
|
*/
|
|
7
|
-
export function compileEnvFiles(
|
|
8
|
-
["production", "development", "test"].forEach((mode) => fs.appendFileSync(path.join(
|
|
7
|
+
export function compileEnvFiles(buildOpts) {
|
|
8
|
+
["production", "development", "test"].forEach((mode) => fs.appendFileSync(path.join(buildOpts.outputDir, `.env.mjs`), `export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, buildOpts))};\n`));
|
|
9
9
|
}
|
|
@@ -7,4 +7,7 @@ import { type SgNode } from "@ast-grep/napi";
|
|
|
7
7
|
* So we wrap `require(optionalDep)` in a try/catch (if not already present).
|
|
8
8
|
*/
|
|
9
9
|
export declare const optionalDepRule = "\nrule:\n pattern: $$$LHS = require($$$REQ)\n has:\n pattern: $MOD\n kind: string_fragment\n stopBy: end\n regex: ^caniuse-lite(/|$)\n not:\n inside:\n kind: try_statement\n stopBy: end\n\nfix: |-\n try {\n $$$LHS = require($$$REQ);\n } catch {\n throw new Error('The optional dependency \"$MOD\" is not installed');\n }\n";
|
|
10
|
-
export declare function patchOptionalDependencies(root: SgNode):
|
|
10
|
+
export declare function patchOptionalDependencies(root: SgNode): {
|
|
11
|
+
edits: import("@ast-grep/napi").Edit[];
|
|
12
|
+
matches: SgNode<import("@ast-grep/napi/types/staticTypes.js").TypesMap, import("@ast-grep/napi/types/staticTypes.js").Kinds<import("@ast-grep/napi/types/staticTypes.js").TypesMap>>[];
|
|
13
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyRule } from "./util.js";
|
|
2
2
|
/**
|
|
3
3
|
* Handle optional dependencies.
|
|
4
4
|
*
|
|
@@ -27,5 +27,5 @@ fix: |-
|
|
|
27
27
|
}
|
|
28
28
|
`;
|
|
29
29
|
export function patchOptionalDependencies(root) {
|
|
30
|
-
return
|
|
30
|
+
return applyRule(optionalDepRule, root);
|
|
31
31
|
}
|
|
@@ -7,7 +7,7 @@ export type RuleConfig = NapiConfig & {
|
|
|
7
7
|
fix?: string;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Returns the `Edit`s for an ast-grep rule in yaml format
|
|
10
|
+
* Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format
|
|
11
11
|
*
|
|
12
12
|
* The rule must have a `fix` to rewrite the matched node.
|
|
13
13
|
*
|
|
@@ -16,11 +16,22 @@ export type RuleConfig = NapiConfig & {
|
|
|
16
16
|
* @param rule The rule. Either a yaml string or an instance of `RuleConfig`
|
|
17
17
|
* @param root The root node
|
|
18
18
|
* @param once only apply once
|
|
19
|
-
* @returns A list of edits.
|
|
19
|
+
* @returns A list of edits and a list of matches.
|
|
20
20
|
*/
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function applyRule(rule: string | RuleConfig, root: SgNode, { once }?: {
|
|
22
22
|
once?: boolean | undefined;
|
|
23
|
-
}):
|
|
23
|
+
}): {
|
|
24
|
+
edits: Edit[];
|
|
25
|
+
matches: SgNode<import("@ast-grep/napi/types/staticTypes").TypesMap, import("@ast-grep/napi/types/staticTypes").Kinds<import("@ast-grep/napi/types/staticTypes").TypesMap>>[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parse a file and obtain its root.
|
|
29
|
+
*
|
|
30
|
+
* @param path The file path
|
|
31
|
+
* @param lang The language to parse. Defaults to TypeScript.
|
|
32
|
+
* @returns The root for the file.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseFile(path: string, lang?: Lang): SgNode<import("@ast-grep/napi/types/staticTypes").TypesMap, import("@ast-grep/napi/types/staticTypes").Kinds<import("@ast-grep/napi/types/staticTypes").TypesMap>>;
|
|
24
35
|
/**
|
|
25
36
|
* Patches the code from by applying the rule.
|
|
26
37
|
*
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { Lang, parse } from "@ast-grep/napi";
|
|
2
3
|
import yaml from "yaml";
|
|
3
4
|
/**
|
|
4
|
-
* Returns the `Edit`s for an ast-grep rule in yaml format
|
|
5
|
+
* Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format
|
|
5
6
|
*
|
|
6
7
|
* The rule must have a `fix` to rewrite the matched node.
|
|
7
8
|
*
|
|
@@ -10,9 +11,9 @@ import yaml from "yaml";
|
|
|
10
11
|
* @param rule The rule. Either a yaml string or an instance of `RuleConfig`
|
|
11
12
|
* @param root The root node
|
|
12
13
|
* @param once only apply once
|
|
13
|
-
* @returns A list of edits.
|
|
14
|
+
* @returns A list of edits and a list of matches.
|
|
14
15
|
*/
|
|
15
|
-
export function
|
|
16
|
+
export function applyRule(rule, root, { once = false } = {}) {
|
|
16
17
|
const ruleConfig = typeof rule === "string" ? yaml.parse(rule) : rule;
|
|
17
18
|
if (ruleConfig.transform) {
|
|
18
19
|
throw new Error("transform is not supported");
|
|
@@ -33,7 +34,17 @@ export function getRuleEdits(rule, root, { once = false } = {}) {
|
|
|
33
34
|
.join(""))
|
|
34
35
|
.replace(/\$([A-Z0-9_]+)/g, (m, name) => match.getMatch(name)?.text() ?? m)));
|
|
35
36
|
});
|
|
36
|
-
return edits;
|
|
37
|
+
return { edits, matches };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a file and obtain its root.
|
|
41
|
+
*
|
|
42
|
+
* @param path The file path
|
|
43
|
+
* @param lang The language to parse. Defaults to TypeScript.
|
|
44
|
+
* @returns The root for the file.
|
|
45
|
+
*/
|
|
46
|
+
export function parseFile(path, lang = Lang.TypeScript) {
|
|
47
|
+
return parse(lang, readFileSync(path, { encoding: "utf-8" })).root();
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
39
50
|
* Patches the code from by applying the rule.
|
|
@@ -49,6 +60,6 @@ export function getRuleEdits(rule, root, { once = false } = {}) {
|
|
|
49
60
|
*/
|
|
50
61
|
export function patchCode(code, rule, { lang = Lang.TypeScript, once = false } = {}) {
|
|
51
62
|
const node = parse(lang, code).root();
|
|
52
|
-
const edits =
|
|
63
|
+
const { edits } = applyRule(rule, node, { once });
|
|
53
64
|
return node.commitEdits(edits);
|
|
54
65
|
}
|