@opennextjs/cloudflare 1.4.0 → 1.5.1
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/dist/api/cloudflare-context.d.ts +4 -0
- package/dist/api/config.d.ts +16 -0
- package/dist/api/config.js +8 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/overrides/asset-resolver/index.d.ts +19 -0
- package/dist/api/overrides/asset-resolver/index.js +78 -0
- package/dist/cli/build/build.d.ts +2 -1
- package/dist/cli/build/build.js +4 -7
- package/dist/cli/build/open-next/compile-images.js +10 -2
- package/dist/cli/build/open-next/compile-init.d.ts +2 -1
- package/dist/cli/build/open-next/compile-init.js +4 -1
- package/dist/cli/build/open-next/compile-skew-protection.d.ts +3 -0
- package/dist/cli/build/open-next/compile-skew-protection.js +21 -0
- package/dist/cli/build/open-next/compileDurableObjects.js +1 -4
- package/dist/cli/commands/deploy.js +17 -1
- package/dist/cli/commands/helpers.d.ts +20 -0
- package/dist/cli/commands/helpers.js +37 -0
- package/dist/cli/commands/populate-cache.js +6 -28
- package/dist/cli/commands/skew-protection.d.ts +67 -0
- package/dist/cli/commands/skew-protection.js +193 -0
- package/dist/cli/commands/upload.js +15 -1
- package/dist/cli/index.js +10 -2
- package/dist/cli/templates/images.d.ts +13 -1
- package/dist/cli/templates/images.js +96 -2
- package/dist/cli/templates/init.d.ts +2 -0
- package/dist/cli/templates/init.js +7 -2
- package/dist/cli/templates/skew-protection.d.ts +28 -0
- package/dist/cli/templates/skew-protection.js +57 -0
- package/dist/cli/templates/worker.js +7 -1
- package/dist/cli/utils/run-wrangler.d.ts +18 -1
- package/dist/cli/utils/run-wrangler.js +39 -12
- package/package.json +3 -2
|
@@ -26,6 +26,10 @@ declare global {
|
|
|
26
26
|
NEXT_CACHE_DO_PURGE_BUFFER_TIME_IN_SECONDS?: string;
|
|
27
27
|
CACHE_PURGE_ZONE_ID?: string;
|
|
28
28
|
CACHE_PURGE_API_TOKEN?: string;
|
|
29
|
+
CF_WORKER_NAME?: string;
|
|
30
|
+
CF_PREVIEW_DOMAIN?: string;
|
|
31
|
+
CF_WORKERS_SCRIPTS_API_TOKEN?: string;
|
|
32
|
+
CF_ACCOUNT_ID?: string;
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
export type CloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext> = {
|
package/dist/api/config.d.ts
CHANGED
|
@@ -63,6 +63,18 @@ interface OpenNextConfig extends AwsOpenNextConfig {
|
|
|
63
63
|
* @default false
|
|
64
64
|
*/
|
|
65
65
|
dangerousDisableConfigValidation?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Skew protection.
|
|
68
|
+
*
|
|
69
|
+
* Note: Skew Protection is experimental and might break on minor releases.
|
|
70
|
+
*
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
skewProtection?: {
|
|
74
|
+
enabled?: boolean;
|
|
75
|
+
maxNumberOfVersions?: number;
|
|
76
|
+
maxVersionAgeDays?: number;
|
|
77
|
+
};
|
|
66
78
|
};
|
|
67
79
|
}
|
|
68
80
|
/**
|
|
@@ -70,4 +82,8 @@ interface OpenNextConfig extends AwsOpenNextConfig {
|
|
|
70
82
|
* @returns The OpenConfig specific to cloudflare
|
|
71
83
|
*/
|
|
72
84
|
export declare function getOpenNextConfig(buildOpts: BuildOptions): OpenNextConfig;
|
|
85
|
+
/**
|
|
86
|
+
* @returns Unique deployment ID
|
|
87
|
+
*/
|
|
88
|
+
export declare function getDeploymentId(): string;
|
|
73
89
|
export type { OpenNextConfig };
|
package/dist/api/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import assetResolver from "./overrides/asset-resolver/index.js";
|
|
1
2
|
/**
|
|
2
3
|
* Defines the OpenNext configuration that targets the Cloudflare adapter
|
|
3
4
|
*
|
|
@@ -37,6 +38,7 @@ export function defineCloudflareConfig(config = {}) {
|
|
|
37
38
|
tagCache: resolveTagCache(tagCache),
|
|
38
39
|
queue: resolveQueue(queue),
|
|
39
40
|
},
|
|
41
|
+
assetResolver: () => assetResolver,
|
|
40
42
|
},
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -71,3 +73,9 @@ function resolveCdnInvalidation(value = "dummy") {
|
|
|
71
73
|
export function getOpenNextConfig(buildOpts) {
|
|
72
74
|
return buildOpts.config;
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* @returns Unique deployment ID
|
|
78
|
+
*/
|
|
79
|
+
export function getDeploymentId() {
|
|
80
|
+
return `dpl-${new Date().getTime().toString(36)}`;
|
|
81
|
+
}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from "./cloudflare-context.js";
|
|
2
|
-
export { defineCloudflareConfig, type OpenNextConfig } from "./config.js";
|
|
2
|
+
export { defineCloudflareConfig, getDeploymentId, type OpenNextConfig } from "./config.js";
|
package/dist/api/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from "./cloudflare-context.js";
|
|
2
|
-
export { defineCloudflareConfig } from "./config.js";
|
|
2
|
+
export { defineCloudflareConfig, getDeploymentId } from "./config.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AssetResolver } from "@opennextjs/aws/types/overrides";
|
|
2
|
+
/**
|
|
3
|
+
* Serves assets when `run_worker_first` is set to true.
|
|
4
|
+
*
|
|
5
|
+
* When `run_worker_first` is `false`, the assets are served directly bypassing Next routing.
|
|
6
|
+
*
|
|
7
|
+
* When it is `true`, assets are served from the routing layer. It should be used when assets
|
|
8
|
+
* should be behind the middleware or when skew protection is enabled.
|
|
9
|
+
*
|
|
10
|
+
* See https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first
|
|
11
|
+
*/
|
|
12
|
+
declare const resolver: AssetResolver;
|
|
13
|
+
/**
|
|
14
|
+
* @param runWorkerFirst `run_worker_first` config
|
|
15
|
+
* @param pathname pathname of the request
|
|
16
|
+
* @returns Whether the user worker runs first
|
|
17
|
+
*/
|
|
18
|
+
export declare function isUserWorkerFirst(runWorkerFirst: boolean | string[] | undefined, pathname: string): boolean;
|
|
19
|
+
export default resolver;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serves assets when `run_worker_first` is set to true.
|
|
4
|
+
*
|
|
5
|
+
* When `run_worker_first` is `false`, the assets are served directly bypassing Next routing.
|
|
6
|
+
*
|
|
7
|
+
* When it is `true`, assets are served from the routing layer. It should be used when assets
|
|
8
|
+
* should be behind the middleware or when skew protection is enabled.
|
|
9
|
+
*
|
|
10
|
+
* See https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first
|
|
11
|
+
*/
|
|
12
|
+
const resolver = {
|
|
13
|
+
name: "cloudflare-asset-resolver",
|
|
14
|
+
async maybeGetAssetResult(event) {
|
|
15
|
+
const { ASSETS } = getCloudflareContext().env;
|
|
16
|
+
if (!ASSETS || !isUserWorkerFirst(globalThis.__ASSETS_RUN_WORKER_FIRST__, event.rawPath)) {
|
|
17
|
+
// Only handle assets when the user worker runs first for the path
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const { method, headers } = event;
|
|
21
|
+
if (method !== "GET" && method != "HEAD") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const url = new URL(event.rawPath, "https://assets.local");
|
|
25
|
+
const response = await ASSETS.fetch(url, {
|
|
26
|
+
headers,
|
|
27
|
+
method,
|
|
28
|
+
});
|
|
29
|
+
if (response.status === 404) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
type: "core",
|
|
34
|
+
statusCode: response.status,
|
|
35
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
36
|
+
// Workers and Node types differ.
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
body: response.body || new ReadableStream(),
|
|
39
|
+
isBase64Encoded: false,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* @param runWorkerFirst `run_worker_first` config
|
|
45
|
+
* @param pathname pathname of the request
|
|
46
|
+
* @returns Whether the user worker runs first
|
|
47
|
+
*/
|
|
48
|
+
export function isUserWorkerFirst(runWorkerFirst, pathname) {
|
|
49
|
+
if (!Array.isArray(runWorkerFirst)) {
|
|
50
|
+
return runWorkerFirst ?? false;
|
|
51
|
+
}
|
|
52
|
+
let hasPositiveMatch = false;
|
|
53
|
+
for (let rule of runWorkerFirst) {
|
|
54
|
+
let isPositiveRule = true;
|
|
55
|
+
if (rule.startsWith("!")) {
|
|
56
|
+
rule = rule.slice(1);
|
|
57
|
+
isPositiveRule = false;
|
|
58
|
+
}
|
|
59
|
+
else if (hasPositiveMatch) {
|
|
60
|
+
// Do not look for more positive rules once we have a match
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// - Escapes special characters
|
|
64
|
+
// - Replaces * with .*
|
|
65
|
+
const match = new RegExp(`^${rule.replace(/([[\]().*+?^$|{}\\])/g, "\\$1").replace("\\*", ".*")}$`).test(pathname);
|
|
66
|
+
if (match) {
|
|
67
|
+
if (isPositiveRule) {
|
|
68
|
+
hasPositiveMatch = true;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Exit early when there is a negative match
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return hasPositiveMatch;
|
|
77
|
+
}
|
|
78
|
+
export default resolver;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as buildHelper from "@opennextjs/aws/build/helper.js";
|
|
2
|
+
import type { Unstable_Config } from "wrangler";
|
|
2
3
|
import { OpenNextConfig } from "../../api/config.js";
|
|
3
4
|
import type { ProjectOptions } from "../project-options.js";
|
|
4
5
|
/**
|
|
@@ -10,4 +11,4 @@ import type { ProjectOptions } from "../project-options.js";
|
|
|
10
11
|
* @param config The OpenNext config
|
|
11
12
|
* @param projectOpts The options for the project
|
|
12
13
|
*/
|
|
13
|
-
export declare function build(options: buildHelper.BuildOptions, config: OpenNextConfig, projectOpts: ProjectOptions): Promise<void>;
|
|
14
|
+
export declare function build(options: buildHelper.BuildOptions, config: OpenNextConfig, projectOpts: ProjectOptions, wranglerConfig: Unstable_Config): Promise<void>;
|
package/dist/cli/build/build.js
CHANGED
|
@@ -10,6 +10,7 @@ import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-ass
|
|
|
10
10
|
import { compileEnvFiles } from "./open-next/compile-env-files.js";
|
|
11
11
|
import { compileImages } from "./open-next/compile-images.js";
|
|
12
12
|
import { compileInit } from "./open-next/compile-init.js";
|
|
13
|
+
import { compileSkewProtection } from "./open-next/compile-skew-protection.js";
|
|
13
14
|
import { compileDurableObjects } from "./open-next/compileDurableObjects.js";
|
|
14
15
|
import { createServerBundle } from "./open-next/createServerBundle.js";
|
|
15
16
|
import { createWranglerConfigIfNotExistent } from "./utils/index.js";
|
|
@@ -23,9 +24,8 @@ import { getVersion } from "./utils/version.js";
|
|
|
23
24
|
* @param config The OpenNext config
|
|
24
25
|
* @param projectOpts The options for the project
|
|
25
26
|
*/
|
|
26
|
-
export async function build(options, config, projectOpts) {
|
|
27
|
+
export async function build(options, config, projectOpts, wranglerConfig) {
|
|
27
28
|
// Do not minify the code so that we can apply string replacement patch.
|
|
28
|
-
// Note that wrangler will still minify the bundle.
|
|
29
29
|
options.minify = false;
|
|
30
30
|
// Pre-build validation
|
|
31
31
|
buildHelper.checkRunningInsideNextjsApp(options);
|
|
@@ -47,14 +47,11 @@ export async function build(options, config, projectOpts) {
|
|
|
47
47
|
// Generate deployable bundle
|
|
48
48
|
printHeader("Generating bundle");
|
|
49
49
|
buildHelper.initOutputDir(options);
|
|
50
|
-
// Compile cache.ts
|
|
51
50
|
compileCache(options);
|
|
52
|
-
// Compile .env files
|
|
53
51
|
compileEnvFiles(options);
|
|
54
|
-
|
|
55
|
-
compileInit(options);
|
|
56
|
-
// Compile image helpers
|
|
52
|
+
compileInit(options, wranglerConfig);
|
|
57
53
|
compileImages(options);
|
|
54
|
+
compileSkewProtection(options, config);
|
|
58
55
|
// Compile middleware
|
|
59
56
|
await createMiddleware(options, { forceOnlyBuildOnce: true });
|
|
60
57
|
createStaticAssets(options, { useBasePath: true });
|
|
@@ -13,6 +13,11 @@ export async function compileImages(options) {
|
|
|
13
13
|
const imagesManifest = fs.existsSync(imagesManifestPath)
|
|
14
14
|
? JSON.parse(fs.readFileSync(imagesManifestPath, { encoding: "utf-8" }))
|
|
15
15
|
: {};
|
|
16
|
+
const __IMAGES_REMOTE_PATTERNS__ = JSON.stringify(imagesManifest?.images?.remotePatterns ?? []);
|
|
17
|
+
const __IMAGES_LOCAL_PATTERNS__ = JSON.stringify(imagesManifest?.images?.localPatterns ?? []);
|
|
18
|
+
const __IMAGES_ALLOW_SVG__ = JSON.stringify(Boolean(imagesManifest?.images?.dangerouslyAllowSVG));
|
|
19
|
+
const __IMAGES_CONTENT_SECURITY_POLICY__ = JSON.stringify(imagesManifest?.images?.contentSecurityPolicy ?? "script-src 'none'; frame-src 'none'; sandbox;");
|
|
20
|
+
const __IMAGES_CONTENT_DISPOSITION__ = JSON.stringify(imagesManifest?.images?.contentDispositionType ?? "attachment");
|
|
16
21
|
await build({
|
|
17
22
|
entryPoints: [imagesPath],
|
|
18
23
|
outdir: path.join(options.outputDir, "cloudflare"),
|
|
@@ -22,8 +27,11 @@ export async function compileImages(options) {
|
|
|
22
27
|
target: "esnext",
|
|
23
28
|
platform: "node",
|
|
24
29
|
define: {
|
|
25
|
-
__IMAGES_REMOTE_PATTERNS__
|
|
26
|
-
__IMAGES_LOCAL_PATTERNS__
|
|
30
|
+
__IMAGES_REMOTE_PATTERNS__,
|
|
31
|
+
__IMAGES_LOCAL_PATTERNS__,
|
|
32
|
+
__IMAGES_ALLOW_SVG__,
|
|
33
|
+
__IMAGES_CONTENT_SECURITY_POLICY__,
|
|
34
|
+
__IMAGES_CONTENT_DISPOSITION__,
|
|
27
35
|
},
|
|
28
36
|
});
|
|
29
37
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
|
|
2
|
+
import type { Unstable_Config } from "wrangler";
|
|
2
3
|
/**
|
|
3
4
|
* Compiles the initialization code for the workerd runtime
|
|
4
5
|
*/
|
|
5
|
-
export declare function compileInit(options: BuildOptions): Promise<void>;
|
|
6
|
+
export declare function compileInit(options: BuildOptions, wranglerConfig: Unstable_Config): Promise<void>;
|
|
@@ -5,12 +5,13 @@ import { build } from "esbuild";
|
|
|
5
5
|
/**
|
|
6
6
|
* Compiles the initialization code for the workerd runtime
|
|
7
7
|
*/
|
|
8
|
-
export async function compileInit(options) {
|
|
8
|
+
export async function compileInit(options, wranglerConfig) {
|
|
9
9
|
const currentDir = path.join(path.dirname(fileURLToPath(import.meta.url)));
|
|
10
10
|
const templatesDir = path.join(currentDir, "../../templates");
|
|
11
11
|
const initPath = path.join(templatesDir, "init.js");
|
|
12
12
|
const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
|
|
13
13
|
const basePath = nextConfig.basePath ?? "";
|
|
14
|
+
const deploymentId = nextConfig.deploymentId ?? "";
|
|
14
15
|
await build({
|
|
15
16
|
entryPoints: [initPath],
|
|
16
17
|
outdir: path.join(options.outputDir, "cloudflare"),
|
|
@@ -22,6 +23,8 @@ export async function compileInit(options) {
|
|
|
22
23
|
define: {
|
|
23
24
|
__BUILD_TIMESTAMP_MS__: JSON.stringify(Date.now()),
|
|
24
25
|
__NEXT_BASE_PATH__: JSON.stringify(basePath),
|
|
26
|
+
__ASSETS_RUN_WORKER_FIRST__: JSON.stringify(wranglerConfig.assets?.run_worker_first ?? false),
|
|
27
|
+
__DEPLOYMENT_ID__: JSON.stringify(deploymentId),
|
|
25
28
|
},
|
|
26
29
|
});
|
|
27
30
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { build } from "esbuild";
|
|
4
|
+
export async function compileSkewProtection(options, config) {
|
|
5
|
+
const currentDir = path.join(path.dirname(fileURLToPath(import.meta.url)));
|
|
6
|
+
const templatesDir = path.join(currentDir, "../../templates");
|
|
7
|
+
const initPath = path.join(templatesDir, "skew-protection.js");
|
|
8
|
+
const skewProtectionEnabled = config.cloudflare?.skewProtection?.enabled === true;
|
|
9
|
+
await build({
|
|
10
|
+
entryPoints: [initPath],
|
|
11
|
+
outdir: path.join(options.outputDir, "cloudflare"),
|
|
12
|
+
bundle: false,
|
|
13
|
+
minify: false,
|
|
14
|
+
format: "esm",
|
|
15
|
+
target: "esnext",
|
|
16
|
+
platform: "node",
|
|
17
|
+
define: {
|
|
18
|
+
__SKEW_PROTECTION_ENABLED__: JSON.stringify(skewProtectionEnabled),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -9,10 +9,7 @@ export function compileDurableObjects(buildOpts) {
|
|
|
9
9
|
_require.resolve("@opennextjs/cloudflare/durable-objects/sharded-tag-cache"),
|
|
10
10
|
_require.resolve("@opennextjs/cloudflare/durable-objects/bucket-cache-purge"),
|
|
11
11
|
];
|
|
12
|
-
const
|
|
13
|
-
const baseManifestPath = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
|
|
14
|
-
// We need to change the type in aws
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const baseManifestPath = path.join(buildOpts.outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
|
|
16
13
|
const prerenderManifest = loadPrerenderManifest(baseManifestPath);
|
|
17
14
|
const previewModeId = prerenderManifest.preview.previewModeId;
|
|
18
15
|
const BUILD_ID = loadBuildId(baseManifestPath);
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
+
import { DEPLOYMENT_MAPPING_ENV_NAME } from "../templates/skew-protection.js";
|
|
1
2
|
import { getWranglerEnvironmentFlag, runWrangler } from "../utils/run-wrangler.js";
|
|
3
|
+
import { getEnvFromPlatformProxy, quoteShellMeta } from "./helpers.js";
|
|
2
4
|
import { populateCache } from "./populate-cache.js";
|
|
5
|
+
import { getDeploymentMapping } from "./skew-protection.js";
|
|
3
6
|
export async function deploy(options, config, deployOptions) {
|
|
7
|
+
const envVars = await getEnvFromPlatformProxy({
|
|
8
|
+
// TODO: Pass the configPath, update everywhere applicable
|
|
9
|
+
environment: getWranglerEnvironmentFlag(deployOptions.passthroughArgs),
|
|
10
|
+
});
|
|
11
|
+
const deploymentMapping = await getDeploymentMapping(options, config, envVars);
|
|
4
12
|
await populateCache(options, config, {
|
|
5
13
|
target: "remote",
|
|
6
14
|
environment: getWranglerEnvironmentFlag(deployOptions.passthroughArgs),
|
|
7
15
|
cacheChunkSize: deployOptions.cacheChunkSize,
|
|
8
16
|
});
|
|
9
|
-
runWrangler(options, [
|
|
17
|
+
runWrangler(options, [
|
|
18
|
+
"deploy",
|
|
19
|
+
...deployOptions.passthroughArgs,
|
|
20
|
+
...(deploymentMapping
|
|
21
|
+
? [`--var ${DEPLOYMENT_MAPPING_ENV_NAME}:${quoteShellMeta(JSON.stringify(deploymentMapping))}`]
|
|
22
|
+
: []),
|
|
23
|
+
], {
|
|
24
|
+
logging: "all",
|
|
25
|
+
});
|
|
10
26
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type GetPlatformProxyOptions } from "wrangler";
|
|
2
|
+
export type WorkerEnvVar = Record<keyof CloudflareEnv, string | undefined>;
|
|
3
|
+
/**
|
|
4
|
+
* Return the string env vars from the worker.
|
|
5
|
+
*
|
|
6
|
+
* @param options Options to pass to `getPlatformProxy`, i.e. to set the environment
|
|
7
|
+
* @returns the env vars
|
|
8
|
+
*/
|
|
9
|
+
export declare function getEnvFromPlatformProxy(options: GetPlatformProxyOptions): Promise<WorkerEnvVar>;
|
|
10
|
+
/**
|
|
11
|
+
* Escape shell metacharacters.
|
|
12
|
+
*
|
|
13
|
+
* When `spawnSync` is invoked with `shell: true`, metacharacters need to be escaped.
|
|
14
|
+
*
|
|
15
|
+
* Based on https://github.com/ljharb/shell-quote/blob/main/quote.js
|
|
16
|
+
*
|
|
17
|
+
* @param arg
|
|
18
|
+
* @returns escaped arg
|
|
19
|
+
*/
|
|
20
|
+
export declare function quoteShellMeta(arg: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getPlatformProxy } from "wrangler";
|
|
2
|
+
/**
|
|
3
|
+
* Return the string env vars from the worker.
|
|
4
|
+
*
|
|
5
|
+
* @param options Options to pass to `getPlatformProxy`, i.e. to set the environment
|
|
6
|
+
* @returns the env vars
|
|
7
|
+
*/
|
|
8
|
+
export async function getEnvFromPlatformProxy(options) {
|
|
9
|
+
const envVars = {};
|
|
10
|
+
const proxy = await getPlatformProxy(options);
|
|
11
|
+
Object.entries(proxy.env).forEach(([key, value]) => {
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
envVars[key] = value;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
await proxy.dispose();
|
|
17
|
+
return envVars;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Escape shell metacharacters.
|
|
21
|
+
*
|
|
22
|
+
* When `spawnSync` is invoked with `shell: true`, metacharacters need to be escaped.
|
|
23
|
+
*
|
|
24
|
+
* Based on https://github.com/ljharb/shell-quote/blob/main/quote.js
|
|
25
|
+
*
|
|
26
|
+
* @param arg
|
|
27
|
+
* @returns escaped arg
|
|
28
|
+
*/
|
|
29
|
+
export function quoteShellMeta(arg) {
|
|
30
|
+
if (/["\s]/.test(arg) && !/'/.test(arg)) {
|
|
31
|
+
return `'${arg.replace(/(['\\])/g, "\\$1")}'`;
|
|
32
|
+
}
|
|
33
|
+
if (/["'\s]/.test(arg)) {
|
|
34
|
+
return `"${arg.replace(/(["\\$`!])/g, "\\$1")}"`;
|
|
35
|
+
}
|
|
36
|
+
return arg.replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2");
|
|
37
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import logger from "@opennextjs/aws/logger.js";
|
|
4
4
|
import { globSync } from "glob";
|
|
5
5
|
import { tqdm } from "ts-tqdm";
|
|
6
|
-
import {
|
|
6
|
+
import { unstable_readConfig } from "wrangler";
|
|
7
7
|
import { BINDING_NAME as KV_CACHE_BINDING_NAME, NAME as KV_CACHE_NAME, PREFIX_ENV_NAME as KV_CACHE_PREFIX_ENV_NAME, } from "../../api/overrides/incremental-cache/kv-incremental-cache.js";
|
|
8
8
|
import { BINDING_NAME as R2_CACHE_BINDING_NAME, NAME as R2_CACHE_NAME, PREFIX_ENV_NAME as R2_CACHE_PREFIX_ENV_NAME, } from "../../api/overrides/incremental-cache/r2-incremental-cache.js";
|
|
9
9
|
import { CACHE_DIR as STATIC_ASSETS_CACHE_DIR, NAME as STATIC_ASSETS_CACHE_NAME, } from "../../api/overrides/incremental-cache/static-assets-incremental-cache.js";
|
|
@@ -11,6 +11,7 @@ import { computeCacheKey } from "../../api/overrides/internal.js";
|
|
|
11
11
|
import { BINDING_NAME as D1_TAG_BINDING_NAME, NAME as D1_TAG_NAME, } from "../../api/overrides/tag-cache/d1-next-tag-cache.js";
|
|
12
12
|
import { normalizePath } from "../build/utils/normalize-path.js";
|
|
13
13
|
import { runWrangler } from "../utils/run-wrangler.js";
|
|
14
|
+
import { getEnvFromPlatformProxy, quoteShellMeta } from "./helpers.js";
|
|
14
15
|
async function resolveCacheName(value) {
|
|
15
16
|
return typeof value === "function" ? (await value()).name : value;
|
|
16
17
|
}
|
|
@@ -50,12 +51,6 @@ export function getCacheAssets(opts) {
|
|
|
50
51
|
}
|
|
51
52
|
return assets;
|
|
52
53
|
}
|
|
53
|
-
async function getPlatformProxyEnv(options, key) {
|
|
54
|
-
const proxy = await getPlatformProxy(options);
|
|
55
|
-
const prefix = proxy.env[key];
|
|
56
|
-
await proxy.dispose();
|
|
57
|
-
return prefix;
|
|
58
|
-
}
|
|
59
54
|
async function populateR2IncrementalCache(options, populateCacheOptions) {
|
|
60
55
|
logger.info("\nPopulating R2 incremental cache...");
|
|
61
56
|
const config = unstable_readConfig({ env: populateCacheOptions.environment });
|
|
@@ -67,7 +62,8 @@ async function populateR2IncrementalCache(options, populateCacheOptions) {
|
|
|
67
62
|
if (!bucket) {
|
|
68
63
|
throw new Error(`R2 binding ${JSON.stringify(R2_CACHE_BINDING_NAME)} should have a 'bucket_name'`);
|
|
69
64
|
}
|
|
70
|
-
const
|
|
65
|
+
const envVars = await getEnvFromPlatformProxy(populateCacheOptions);
|
|
66
|
+
const prefix = envVars[R2_CACHE_PREFIX_ENV_NAME];
|
|
71
67
|
const assets = getCacheAssets(options);
|
|
72
68
|
for (const { fullPath, key, buildId, isFetch } of tqdm(assets)) {
|
|
73
69
|
const cacheKey = computeCacheKey(key, {
|
|
@@ -93,7 +89,8 @@ async function populateKVIncrementalCache(options, populateCacheOptions) {
|
|
|
93
89
|
if (!binding) {
|
|
94
90
|
throw new Error(`No KV binding ${JSON.stringify(KV_CACHE_BINDING_NAME)} found!`);
|
|
95
91
|
}
|
|
96
|
-
const
|
|
92
|
+
const envVars = await getEnvFromPlatformProxy(populateCacheOptions);
|
|
93
|
+
const prefix = envVars[KV_CACHE_PREFIX_ENV_NAME];
|
|
97
94
|
const assets = getCacheAssets(options);
|
|
98
95
|
const chunkSize = Math.max(1, populateCacheOptions.cacheChunkSize ?? 25);
|
|
99
96
|
const totalChunks = Math.ceil(assets.length / chunkSize);
|
|
@@ -171,22 +168,3 @@ export async function populateCache(options, config, populateCacheOptions) {
|
|
|
171
168
|
}
|
|
172
169
|
}
|
|
173
170
|
}
|
|
174
|
-
/**
|
|
175
|
-
* Escape shell metacharacters.
|
|
176
|
-
*
|
|
177
|
-
* When `spawnSync` is invoked with `shell: true`, metacharacters need to be escaped.
|
|
178
|
-
*
|
|
179
|
-
* Based on https://github.com/ljharb/shell-quote/blob/main/quote.js
|
|
180
|
-
*
|
|
181
|
-
* @param arg
|
|
182
|
-
* @returns escaped arg
|
|
183
|
-
*/
|
|
184
|
-
function quoteShellMeta(arg) {
|
|
185
|
-
if (/["\s]/.test(arg) && !/'/.test(arg)) {
|
|
186
|
-
return `'${arg.replace(/(['\\])/g, "\\$1")}'`;
|
|
187
|
-
}
|
|
188
|
-
if (/["'\s]/.test(arg)) {
|
|
189
|
-
return `"${arg.replace(/(["\\$`!])/g, "\\$1")}"`;
|
|
190
|
-
}
|
|
191
|
-
return arg.replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, "$1\\$2");
|
|
192
|
-
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* We need to maintain a mapping of deployment id to worker version for skew protection.
|
|
3
|
+
*
|
|
4
|
+
* The mapping is used to request the correct version of the workers when Next attaches a deployment id to a request.
|
|
5
|
+
*
|
|
6
|
+
* The mapping is stored in a worker en var:
|
|
7
|
+
*
|
|
8
|
+
* {
|
|
9
|
+
* latestDepId: "current",
|
|
10
|
+
* depIdx: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
11
|
+
* depIdy: "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
|
|
12
|
+
* depIdz: "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Note that the latest version is not known at build time as the version id only gets created on deployment.
|
|
16
|
+
* This is why we use the "current" placeholder.
|
|
17
|
+
*
|
|
18
|
+
* When a new version is deployed:
|
|
19
|
+
* - "current" is replaced with the latest version of the Worker
|
|
20
|
+
* - a new entry is added for the new deployment id with the "current" version
|
|
21
|
+
*/
|
|
22
|
+
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
|
|
23
|
+
import { Cloudflare } from "cloudflare";
|
|
24
|
+
import type { OpenNextConfig } from "../../api";
|
|
25
|
+
import type { WorkerEnvVar } from "./helpers.js";
|
|
26
|
+
/**
|
|
27
|
+
* Compute the deployment mapping for a deployment.
|
|
28
|
+
*
|
|
29
|
+
* @param options Build options
|
|
30
|
+
* @param config OpenNext config
|
|
31
|
+
* @param envVars Environment variables
|
|
32
|
+
* @returns Deployment mapping or undefined
|
|
33
|
+
*/
|
|
34
|
+
export declare function getDeploymentMapping(options: BuildOptions, config: OpenNextConfig, envVars: WorkerEnvVar): Promise<Record<string, string> | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Update an existing deployment mapping:
|
|
37
|
+
* - Replace the "current" version with the latest deployed version
|
|
38
|
+
* - Add a "current" version for the current deployment ID
|
|
39
|
+
* - Remove versions that are not passed in
|
|
40
|
+
*
|
|
41
|
+
* @param mapping Existing mapping
|
|
42
|
+
* @param versions Versions ordered by descending time
|
|
43
|
+
* @param deploymentId Deployment ID
|
|
44
|
+
* @returns The updated mapping
|
|
45
|
+
*/
|
|
46
|
+
export declare function updateDeploymentMapping(mapping: Record<string, string>, versions: {
|
|
47
|
+
id: string;
|
|
48
|
+
}[], deploymentId: string): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Retrieve the versions for the script
|
|
51
|
+
*
|
|
52
|
+
* @param scriptName The name of the worker script
|
|
53
|
+
* @param options.client A Cloudflare API client
|
|
54
|
+
* @param options.accountId The Cloudflare account id
|
|
55
|
+
* @param options.afterTimeMs Only list version more recent than this time - default to 7 days
|
|
56
|
+
* @param options.maxNumberOfVersions The maximum number of version to return - default to 20 versions.
|
|
57
|
+
* @returns A list of id and creation date ordered by descending creation date
|
|
58
|
+
*/
|
|
59
|
+
export declare function listWorkerVersions(scriptName: string, options: {
|
|
60
|
+
client: Cloudflare;
|
|
61
|
+
accountId: string;
|
|
62
|
+
afterTimeMs?: number;
|
|
63
|
+
maxNumberOfVersions?: number;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
id: string;
|
|
66
|
+
createdOnMs: number;
|
|
67
|
+
}[]>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* We need to maintain a mapping of deployment id to worker version for skew protection.
|
|
3
|
+
*
|
|
4
|
+
* The mapping is used to request the correct version of the workers when Next attaches a deployment id to a request.
|
|
5
|
+
*
|
|
6
|
+
* The mapping is stored in a worker en var:
|
|
7
|
+
*
|
|
8
|
+
* {
|
|
9
|
+
* latestDepId: "current",
|
|
10
|
+
* depIdx: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
11
|
+
* depIdy: "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
|
|
12
|
+
* depIdz: "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Note that the latest version is not known at build time as the version id only gets created on deployment.
|
|
16
|
+
* This is why we use the "current" placeholder.
|
|
17
|
+
*
|
|
18
|
+
* When a new version is deployed:
|
|
19
|
+
* - "current" is replaced with the latest version of the Worker
|
|
20
|
+
* - a new entry is added for the new deployment id with the "current" version
|
|
21
|
+
*/
|
|
22
|
+
// re-enable when types are fixed in the cloudflare lib
|
|
23
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { loadConfig } from "@opennextjs/aws/adapters/config/util.js";
|
|
26
|
+
import logger from "@opennextjs/aws/logger.js";
|
|
27
|
+
import { Cloudflare, NotFoundError } from "cloudflare";
|
|
28
|
+
import { CURRENT_VERSION_ID, DEPLOYMENT_MAPPING_ENV_NAME } from "../templates/skew-protection.js";
|
|
29
|
+
/** Maximum number of versions to list */
|
|
30
|
+
const MAX_NUMBER_OF_VERSIONS = 20;
|
|
31
|
+
/** Maximum age of versions to list */
|
|
32
|
+
const MAX_VERSION_AGE_DAYS = 7;
|
|
33
|
+
const MS_PER_DAY = 24 * 3600 * 1000;
|
|
34
|
+
/**
|
|
35
|
+
* Compute the deployment mapping for a deployment.
|
|
36
|
+
*
|
|
37
|
+
* @param options Build options
|
|
38
|
+
* @param config OpenNext config
|
|
39
|
+
* @param envVars Environment variables
|
|
40
|
+
* @returns Deployment mapping or undefined
|
|
41
|
+
*/
|
|
42
|
+
export async function getDeploymentMapping(options, config, envVars) {
|
|
43
|
+
if (config.cloudflare?.skewProtection?.enabled !== true) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
|
|
47
|
+
const deploymentId = nextConfig.deploymentId;
|
|
48
|
+
if (!deploymentId) {
|
|
49
|
+
logger.error("Deployment ID should be set in the Next config when skew protection is enabled");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
if (!envVars.CF_WORKER_NAME) {
|
|
53
|
+
logger.error("CF_WORKER_NAME should be set when skew protection is enabled");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (!envVars.CF_PREVIEW_DOMAIN) {
|
|
57
|
+
logger.error("CF_PREVIEW_DOMAIN should be set when skew protection is enabled");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (!envVars.CF_WORKERS_SCRIPTS_API_TOKEN) {
|
|
61
|
+
logger.error("CF_WORKERS_SCRIPTS_API_TOKEN should be set when skew protection is enabled");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (!envVars.CF_ACCOUNT_ID) {
|
|
65
|
+
logger.error("CF_ACCOUNT_ID should be set when skew protection is enabled");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const apiToken = envVars.CF_WORKERS_SCRIPTS_API_TOKEN;
|
|
69
|
+
const accountId = envVars.CF_ACCOUNT_ID;
|
|
70
|
+
const client = new Cloudflare({ apiToken });
|
|
71
|
+
const scriptName = envVars.CF_WORKER_NAME;
|
|
72
|
+
const deployedVersions = await listWorkerVersions(scriptName, {
|
|
73
|
+
client,
|
|
74
|
+
accountId,
|
|
75
|
+
maxNumberOfVersions: config.cloudflare?.skewProtection?.maxNumberOfVersions,
|
|
76
|
+
afterTimeMs: config.cloudflare?.skewProtection?.maxVersionAgeDays
|
|
77
|
+
? Date.now() - config.cloudflare?.skewProtection?.maxVersionAgeDays * MS_PER_DAY
|
|
78
|
+
: undefined,
|
|
79
|
+
});
|
|
80
|
+
const existingMapping = deployedVersions.length === 0
|
|
81
|
+
? {}
|
|
82
|
+
: await getExistingDeploymentMapping(scriptName, deployedVersions[0].id, {
|
|
83
|
+
client,
|
|
84
|
+
accountId,
|
|
85
|
+
});
|
|
86
|
+
if (deploymentId in existingMapping) {
|
|
87
|
+
logger.error(`The deploymentId "${deploymentId}" has been used previously, update your next config and rebuild`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const mapping = updateDeploymentMapping(existingMapping, deployedVersions, deploymentId);
|
|
91
|
+
return mapping;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Update an existing deployment mapping:
|
|
95
|
+
* - Replace the "current" version with the latest deployed version
|
|
96
|
+
* - Add a "current" version for the current deployment ID
|
|
97
|
+
* - Remove versions that are not passed in
|
|
98
|
+
*
|
|
99
|
+
* @param mapping Existing mapping
|
|
100
|
+
* @param versions Versions ordered by descending time
|
|
101
|
+
* @param deploymentId Deployment ID
|
|
102
|
+
* @returns The updated mapping
|
|
103
|
+
*/
|
|
104
|
+
export function updateDeploymentMapping(mapping, versions, deploymentId) {
|
|
105
|
+
const newMapping = { [deploymentId]: CURRENT_VERSION_ID };
|
|
106
|
+
const versionIds = new Set(versions.map((v) => v.id));
|
|
107
|
+
for (const [deployment, version] of Object.entries(mapping)) {
|
|
108
|
+
if (version === CURRENT_VERSION_ID && versions.length > 0) {
|
|
109
|
+
newMapping[deployment] = versions[0].id;
|
|
110
|
+
}
|
|
111
|
+
else if (versionIds.has(version)) {
|
|
112
|
+
newMapping[deployment] = version;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return newMapping;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Retrieve the deployment mapping from the last deployed worker.
|
|
119
|
+
*
|
|
120
|
+
* NOTE: it is retrieved from the DEPLOYMENT_MAPPING_ENV_NAME env var.
|
|
121
|
+
*
|
|
122
|
+
* @param scriptName The name of the worker script
|
|
123
|
+
* @param versionId The version Id to retrieve
|
|
124
|
+
* @param options.client A Cloudflare API client
|
|
125
|
+
* @param options.accountId The Cloudflare account id
|
|
126
|
+
* @returns The deployment mapping
|
|
127
|
+
*/
|
|
128
|
+
async function getExistingDeploymentMapping(scriptName, versionId, options) {
|
|
129
|
+
// See https://github.com/cloudflare/cloudflare-typescript/issues/2652
|
|
130
|
+
const bindings = (await getVersionDetail(scriptName, versionId, options)).resources.bindings ?? [];
|
|
131
|
+
for (const binding of bindings) {
|
|
132
|
+
if (binding.name === DEPLOYMENT_MAPPING_ENV_NAME && binding.type == "plain_text") {
|
|
133
|
+
return JSON.parse(binding.text);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Retrieve the details of the version of a script
|
|
140
|
+
*
|
|
141
|
+
* @param scriptName The name of the worker script
|
|
142
|
+
* @param versionId The version Id to retrieve
|
|
143
|
+
* @param options.client A Cloudflare API client
|
|
144
|
+
* @param options.accountId The Cloudflare account id
|
|
145
|
+
|
|
146
|
+
* @returns the version information
|
|
147
|
+
*/
|
|
148
|
+
async function getVersionDetail(scriptName, versionId, options) {
|
|
149
|
+
const { client, accountId } = options;
|
|
150
|
+
return await client.workers.scripts.versions.get(scriptName, versionId, {
|
|
151
|
+
account_id: accountId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Retrieve the versions for the script
|
|
156
|
+
*
|
|
157
|
+
* @param scriptName The name of the worker script
|
|
158
|
+
* @param options.client A Cloudflare API client
|
|
159
|
+
* @param options.accountId The Cloudflare account id
|
|
160
|
+
* @param options.afterTimeMs Only list version more recent than this time - default to 7 days
|
|
161
|
+
* @param options.maxNumberOfVersions The maximum number of version to return - default to 20 versions.
|
|
162
|
+
* @returns A list of id and creation date ordered by descending creation date
|
|
163
|
+
*/
|
|
164
|
+
export async function listWorkerVersions(scriptName, options) {
|
|
165
|
+
const versions = [];
|
|
166
|
+
const { client, accountId, afterTimeMs = new Date().getTime() - MAX_VERSION_AGE_DAYS * 24 * 3600 * 1000, maxNumberOfVersions = MAX_NUMBER_OF_VERSIONS, } = options;
|
|
167
|
+
try {
|
|
168
|
+
for await (const version of client.workers.scripts.versions.list(scriptName, {
|
|
169
|
+
account_id: accountId,
|
|
170
|
+
})) {
|
|
171
|
+
const id = version.id;
|
|
172
|
+
const createdOn = version.metadata?.created_on;
|
|
173
|
+
if (id && createdOn) {
|
|
174
|
+
const createdOnMs = new Date(createdOn).getTime();
|
|
175
|
+
if (createdOnMs < afterTimeMs) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
versions.push({ id, createdOnMs });
|
|
179
|
+
if (versions.length >= maxNumberOfVersions) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (e instanceof NotFoundError && e.status === 404) {
|
|
187
|
+
// The worker has not been deployed before, no previous versions.
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
return versions.sort((a, b) => b.createdOnMs - a.createdOnMs);
|
|
193
|
+
}
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
import { DEPLOYMENT_MAPPING_ENV_NAME } from "../templates/skew-protection.js";
|
|
1
2
|
import { getWranglerEnvironmentFlag, runWrangler } from "../utils/run-wrangler.js";
|
|
3
|
+
import { getEnvFromPlatformProxy, quoteShellMeta } from "./helpers.js";
|
|
2
4
|
import { populateCache } from "./populate-cache.js";
|
|
5
|
+
import { getDeploymentMapping } from "./skew-protection.js";
|
|
3
6
|
export async function upload(options, config, uploadOptions) {
|
|
7
|
+
const envVars = await getEnvFromPlatformProxy({
|
|
8
|
+
// TODO: Pass the configPath, update everywhere applicable
|
|
9
|
+
environment: getWranglerEnvironmentFlag(uploadOptions.passthroughArgs),
|
|
10
|
+
});
|
|
11
|
+
const deploymentMapping = await getDeploymentMapping(options, config, envVars);
|
|
4
12
|
await populateCache(options, config, {
|
|
5
13
|
target: "remote",
|
|
6
14
|
environment: getWranglerEnvironmentFlag(uploadOptions.passthroughArgs),
|
|
7
15
|
cacheChunkSize: uploadOptions.cacheChunkSize,
|
|
8
16
|
});
|
|
9
|
-
runWrangler(options, [
|
|
17
|
+
runWrangler(options, [
|
|
18
|
+
"versions upload",
|
|
19
|
+
...uploadOptions.passthroughArgs,
|
|
20
|
+
...(deploymentMapping
|
|
21
|
+
? [`--var ${DEPLOYMENT_MAPPING_ENV_NAME}:${quoteShellMeta(JSON.stringify(deploymentMapping))}`]
|
|
22
|
+
: []),
|
|
23
|
+
], { logging: "all" });
|
|
10
24
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
|
|
|
5
5
|
import { normalizeOptions } from "@opennextjs/aws/build/helper.js";
|
|
6
6
|
import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
|
|
7
7
|
import logger from "@opennextjs/aws/logger.js";
|
|
8
|
+
import { unstable_readConfig } from "wrangler";
|
|
8
9
|
import { getArgs } from "./args.js";
|
|
9
10
|
import { build } from "./build/build.js";
|
|
10
11
|
import { createOpenNextConfigIfNotExistent, ensureCloudflareConfig } from "./build/utils/index.js";
|
|
@@ -12,6 +13,7 @@ import { deploy } from "./commands/deploy.js";
|
|
|
12
13
|
import { populateCache } from "./commands/populate-cache.js";
|
|
13
14
|
import { preview } from "./commands/preview.js";
|
|
14
15
|
import { upload } from "./commands/upload.js";
|
|
16
|
+
import { getWranglerConfigFlag, getWranglerEnvironmentFlag } from "./utils/run-wrangler.js";
|
|
15
17
|
const nextAppDir = process.cwd();
|
|
16
18
|
async function runCommand(args) {
|
|
17
19
|
printHeader(`Cloudflare ${args.command}`);
|
|
@@ -19,6 +21,7 @@ async function runCommand(args) {
|
|
|
19
21
|
const baseDir = nextAppDir;
|
|
20
22
|
const require = createRequire(import.meta.url);
|
|
21
23
|
const openNextDistDir = path.dirname(require.resolve("@opennextjs/aws/index.js"));
|
|
24
|
+
// TODO: retrieve the compiled version if command != build
|
|
22
25
|
await createOpenNextConfigIfNotExistent(baseDir);
|
|
23
26
|
const { config, buildDir } = await compileOpenNextConfig(baseDir, undefined, {
|
|
24
27
|
compileEdge: true,
|
|
@@ -28,8 +31,13 @@ async function runCommand(args) {
|
|
|
28
31
|
const options = normalizeOptions(config, openNextDistDir, buildDir);
|
|
29
32
|
logger.setLevel(options.debug ? "debug" : "info");
|
|
30
33
|
switch (args.command) {
|
|
31
|
-
case "build":
|
|
32
|
-
|
|
34
|
+
case "build": {
|
|
35
|
+
const argv = process.argv.slice(2);
|
|
36
|
+
const wranglerEnv = getWranglerEnvironmentFlag(argv);
|
|
37
|
+
const wranglerConfigFile = getWranglerConfigFlag(argv);
|
|
38
|
+
const wranglerConfig = unstable_readConfig({ env: wranglerEnv, config: wranglerConfigFile });
|
|
39
|
+
return build(options, config, { ...args, sourceDir: baseDir }, wranglerConfig);
|
|
40
|
+
}
|
|
33
41
|
case "preview":
|
|
34
42
|
return preview(options, config, args);
|
|
35
43
|
case "deploy":
|
|
@@ -15,10 +15,22 @@ export type LocalPattern = {
|
|
|
15
15
|
* Local images (starting with a '/' as fetched using the passed fetcher).
|
|
16
16
|
* Remote images should match the configured remote patterns or a 404 response is returned.
|
|
17
17
|
*/
|
|
18
|
-
export declare function fetchImage(fetcher: Fetcher | undefined, imageUrl: string):
|
|
18
|
+
export declare function fetchImage(fetcher: Fetcher | undefined, imageUrl: string, ctx: ExecutionContext): Promise<Response | undefined>;
|
|
19
19
|
export declare function matchRemotePattern(pattern: RemotePattern, url: URL): boolean;
|
|
20
20
|
export declare function matchLocalPattern(pattern: LocalPattern, url: URL): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Detects the content type by looking at the first few bytes of a file
|
|
23
|
+
*
|
|
24
|
+
* Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155
|
|
25
|
+
*
|
|
26
|
+
* @param buffer The image bytes
|
|
27
|
+
* @returns a content type of undefined for unsupported content
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectContentType(buffer: Uint8Array): "image/svg+xml" | "image/jpeg" | "image/png" | "image/gif" | "image/webp" | "image/avif" | "image/x-icon" | "image/x-icns" | "image/tiff" | "image/bmp" | undefined;
|
|
21
30
|
declare global {
|
|
22
31
|
var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
|
|
23
32
|
var __IMAGES_LOCAL_PATTERNS__: LocalPattern[];
|
|
33
|
+
var __IMAGES_ALLOW_SVG__: boolean;
|
|
34
|
+
var __IMAGES_CONTENT_SECURITY_POLICY__: string;
|
|
35
|
+
var __IMAGES_CONTENT_DISPOSITION__: string;
|
|
24
36
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Local images (starting with a '/' as fetched using the passed fetcher).
|
|
5
5
|
* Remote images should match the configured remote patterns or a 404 response is returned.
|
|
6
6
|
*/
|
|
7
|
-
export function fetchImage(fetcher, imageUrl) {
|
|
7
|
+
export async function fetchImage(fetcher, imageUrl, ctx) {
|
|
8
8
|
// https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208
|
|
9
9
|
if (!imageUrl || imageUrl.length > 3072 || imageUrl.startsWith("//")) {
|
|
10
10
|
return getUrlErrorResponse();
|
|
@@ -46,7 +46,48 @@ export function fetchImage(fetcher, imageUrl) {
|
|
|
46
46
|
if (!__IMAGES_REMOTE_PATTERNS__.some((p) => matchRemotePattern(p, url))) {
|
|
47
47
|
return getUrlErrorResponse();
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
const imgResponse = await fetch(imageUrl, { cf: { cacheEverything: true } });
|
|
50
|
+
if (!imgResponse.body) {
|
|
51
|
+
return imgResponse;
|
|
52
|
+
}
|
|
53
|
+
const buffer = new ArrayBuffer(32);
|
|
54
|
+
try {
|
|
55
|
+
let contentType;
|
|
56
|
+
// body1 is eventually used for the response
|
|
57
|
+
// body2 is used to detect the content type
|
|
58
|
+
const [body1, body2] = imgResponse.body.tee();
|
|
59
|
+
const reader = body2.getReader({ mode: "byob" });
|
|
60
|
+
const { value } = await reader.read(new Uint8Array(buffer));
|
|
61
|
+
// Release resources by calling `reader.cancel()`
|
|
62
|
+
// `ctx.waitUntil` keeps the runtime running until the promise settles without having to wait here.
|
|
63
|
+
ctx.waitUntil(reader.cancel());
|
|
64
|
+
if (value) {
|
|
65
|
+
contentType = detectContentType(value);
|
|
66
|
+
}
|
|
67
|
+
if (!contentType) {
|
|
68
|
+
// Fallback to the sanitized upstream header when the type can not be detected
|
|
69
|
+
// https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L748
|
|
70
|
+
const header = imgResponse.headers.get("content-type") ?? "";
|
|
71
|
+
if (header.startsWith("image/") && !header.includes(",")) {
|
|
72
|
+
contentType = header;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (contentType && !(contentType === SVG && !__IMAGES_ALLOW_SVG__)) {
|
|
76
|
+
const headers = new Headers(imgResponse.headers);
|
|
77
|
+
headers.set("content-type", contentType);
|
|
78
|
+
headers.set("content-disposition", __IMAGES_CONTENT_DISPOSITION__);
|
|
79
|
+
headers.set("content-security-policy", __IMAGES_CONTENT_SECURITY_POLICY__);
|
|
80
|
+
return new Response(body1, { ...imgResponse, headers });
|
|
81
|
+
}
|
|
82
|
+
return new Response('"url" parameter is valid but image type is not allowed', {
|
|
83
|
+
status: 400,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return new Response('"url" parameter is valid but upstream response is invalid', {
|
|
88
|
+
status: 400,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
50
91
|
}
|
|
51
92
|
export function matchRemotePattern(pattern, url) {
|
|
52
93
|
// https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts
|
|
@@ -79,4 +120,57 @@ export function matchLocalPattern(pattern, url) {
|
|
|
79
120
|
function getUrlErrorResponse() {
|
|
80
121
|
return new Response(`"url" parameter is not allowed`, { status: 400 });
|
|
81
122
|
}
|
|
123
|
+
const AVIF = "image/avif";
|
|
124
|
+
const WEBP = "image/webp";
|
|
125
|
+
const PNG = "image/png";
|
|
126
|
+
const JPEG = "image/jpeg";
|
|
127
|
+
const GIF = "image/gif";
|
|
128
|
+
const SVG = "image/svg+xml";
|
|
129
|
+
const ICO = "image/x-icon";
|
|
130
|
+
const ICNS = "image/x-icns";
|
|
131
|
+
const TIFF = "image/tiff";
|
|
132
|
+
const BMP = "image/bmp";
|
|
133
|
+
/**
|
|
134
|
+
* Detects the content type by looking at the first few bytes of a file
|
|
135
|
+
*
|
|
136
|
+
* Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155
|
|
137
|
+
*
|
|
138
|
+
* @param buffer The image bytes
|
|
139
|
+
* @returns a content type of undefined for unsupported content
|
|
140
|
+
*/
|
|
141
|
+
export function detectContentType(buffer) {
|
|
142
|
+
if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
|
|
143
|
+
return JPEG;
|
|
144
|
+
}
|
|
145
|
+
if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) {
|
|
146
|
+
return PNG;
|
|
147
|
+
}
|
|
148
|
+
if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
|
|
149
|
+
return GIF;
|
|
150
|
+
}
|
|
151
|
+
if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) {
|
|
152
|
+
return WEBP;
|
|
153
|
+
}
|
|
154
|
+
if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
|
|
155
|
+
return SVG;
|
|
156
|
+
}
|
|
157
|
+
if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) {
|
|
158
|
+
return SVG;
|
|
159
|
+
}
|
|
160
|
+
if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) {
|
|
161
|
+
return AVIF;
|
|
162
|
+
}
|
|
163
|
+
if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) {
|
|
164
|
+
return ICO;
|
|
165
|
+
}
|
|
166
|
+
if ([0x69, 0x63, 0x6e, 0x73].every((b, i) => buffer[i] === b)) {
|
|
167
|
+
return ICNS;
|
|
168
|
+
}
|
|
169
|
+
if ([0x49, 0x49, 0x2a, 0x00].every((b, i) => buffer[i] === b)) {
|
|
170
|
+
return TIFF;
|
|
171
|
+
}
|
|
172
|
+
if ([0x42, 0x4d].every((b, i) => buffer[i] === b)) {
|
|
173
|
+
return BMP;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
82
176
|
/* eslint-enable no-var */
|
|
@@ -10,4 +10,6 @@ export declare function runWithCloudflareRequestContext(request: Request, env: C
|
|
|
10
10
|
declare global {
|
|
11
11
|
var __BUILD_TIMESTAMP_MS__: number;
|
|
12
12
|
var __NEXT_BASE_PATH__: string;
|
|
13
|
+
var __ASSETS_RUN_WORKER_FIRST__: boolean | string[] | undefined;
|
|
14
|
+
var __DEPLOYMENT_ID__: string;
|
|
13
15
|
}
|
|
@@ -73,8 +73,9 @@ function initRuntime() {
|
|
|
73
73
|
};
|
|
74
74
|
Object.assign(globalThis, {
|
|
75
75
|
Request: CustomRequest,
|
|
76
|
-
__BUILD_TIMESTAMP_MS__
|
|
77
|
-
__NEXT_BASE_PATH__
|
|
76
|
+
__BUILD_TIMESTAMP_MS__,
|
|
77
|
+
__NEXT_BASE_PATH__,
|
|
78
|
+
__ASSETS_RUN_WORKER_FIRST__,
|
|
78
79
|
// The external middleware will use the convertTo function of the `edge` converter
|
|
79
80
|
// by default it will try to fetch the request, but since we are running everything in the same worker
|
|
80
81
|
// we need to use the request as is.
|
|
@@ -113,5 +114,9 @@ function populateProcessEnv(url, env) {
|
|
|
113
114
|
* https://github.com/vercel/next.js/blob/6b1e48080e896e0d44a05fe009cb79d2d3f91774/packages/next/src/server/app-render/action-handler.ts#L307-L316
|
|
114
115
|
*/
|
|
115
116
|
process.env.__NEXT_PRIVATE_ORIGIN = url.origin;
|
|
117
|
+
// `__DEPLOYMENT_ID__` is a string (passed via ESBuild).
|
|
118
|
+
if (__DEPLOYMENT_ID__) {
|
|
119
|
+
process.env.DEPLOYMENT_ID = __DEPLOYMENT_ID__;
|
|
120
|
+
}
|
|
116
121
|
}
|
|
117
122
|
/* eslint-enable no-var */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Name of the env var containing the mapping */
|
|
2
|
+
export declare const DEPLOYMENT_MAPPING_ENV_NAME = "CF_DEPLOYMENT_MAPPING";
|
|
3
|
+
/** Version used for the latest worker */
|
|
4
|
+
export declare const CURRENT_VERSION_ID = "current";
|
|
5
|
+
/**
|
|
6
|
+
* Routes the request to the requested deployment.
|
|
7
|
+
*
|
|
8
|
+
* A specific deployment can be requested via:
|
|
9
|
+
* - the `dpl` search parameter for assets
|
|
10
|
+
* - the `x-deployment-id` for other requests
|
|
11
|
+
*
|
|
12
|
+
* When a specific deployment is requested, we route to that deployment via the preview URLs.
|
|
13
|
+
* See https://developers.cloudflare.com/workers/configuration/previews/
|
|
14
|
+
*
|
|
15
|
+
* When the requested deployment is not supported a 400 response is returned.
|
|
16
|
+
*
|
|
17
|
+
* Notes:
|
|
18
|
+
* - The re-routing is only active for the deployed version of the app (on a custom domain)
|
|
19
|
+
* - Assets are also handled when `run_worker_first` is enabled.
|
|
20
|
+
* See https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first
|
|
21
|
+
*
|
|
22
|
+
* @param request
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
export declare function maybeGetSkewProtectionResponse(request: Request): Promise<Response> | Response | undefined;
|
|
26
|
+
declare global {
|
|
27
|
+
var __SKEW_PROTECTION_ENABLED__: boolean;
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
/** Name of the env var containing the mapping */
|
|
3
|
+
export const DEPLOYMENT_MAPPING_ENV_NAME = "CF_DEPLOYMENT_MAPPING";
|
|
4
|
+
/** Version used for the latest worker */
|
|
5
|
+
export const CURRENT_VERSION_ID = "current";
|
|
6
|
+
/**
|
|
7
|
+
* Routes the request to the requested deployment.
|
|
8
|
+
*
|
|
9
|
+
* A specific deployment can be requested via:
|
|
10
|
+
* - the `dpl` search parameter for assets
|
|
11
|
+
* - the `x-deployment-id` for other requests
|
|
12
|
+
*
|
|
13
|
+
* When a specific deployment is requested, we route to that deployment via the preview URLs.
|
|
14
|
+
* See https://developers.cloudflare.com/workers/configuration/previews/
|
|
15
|
+
*
|
|
16
|
+
* When the requested deployment is not supported a 400 response is returned.
|
|
17
|
+
*
|
|
18
|
+
* Notes:
|
|
19
|
+
* - The re-routing is only active for the deployed version of the app (on a custom domain)
|
|
20
|
+
* - Assets are also handled when `run_worker_first` is enabled.
|
|
21
|
+
* See https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first
|
|
22
|
+
*
|
|
23
|
+
* @param request
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
export function maybeGetSkewProtectionResponse(request) {
|
|
27
|
+
// no early return as esbuild would not treeshake the code.
|
|
28
|
+
if (__SKEW_PROTECTION_ENABLED__) {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
// Skew protection is only active for the latest version of the app served on a custom domain.
|
|
31
|
+
if (url.hostname === "localhost" || url.hostname.endsWith(".workers.dev")) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const requestDeploymentId = request.headers.get("x-deployment-id") ?? url.searchParams.get("dpl");
|
|
35
|
+
if (!requestDeploymentId || requestDeploymentId === process.env.DEPLOYMENT_ID) {
|
|
36
|
+
// The request does not specify a deployment id or it is the current deployment id
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const mapping = process.env[DEPLOYMENT_MAPPING_ENV_NAME]
|
|
40
|
+
? JSON.parse(process.env[DEPLOYMENT_MAPPING_ENV_NAME])
|
|
41
|
+
: {};
|
|
42
|
+
if (!(requestDeploymentId in mapping)) {
|
|
43
|
+
// Unknown deployment id, serve the current version
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const version = mapping[requestDeploymentId];
|
|
47
|
+
if (!version || version === CURRENT_VERSION_ID) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const versionDomain = version.split("-")[0];
|
|
51
|
+
const hostname = `${versionDomain}-${process.env.CF_WORKER_NAME}.${process.env.CF_PREVIEW_DOMAIN}.workers.dev`;
|
|
52
|
+
url.hostname = hostname;
|
|
53
|
+
const requestToOlderDeployment = new Request(url, request);
|
|
54
|
+
return fetch(requestToOlderDeployment);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/* eslint-enable no-var */
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { fetchImage } from "./cloudflare/images.js";
|
|
3
3
|
//@ts-expect-error: Will be resolved by wrangler build
|
|
4
4
|
import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
|
|
5
|
+
//@ts-expect-error: Will be resolved by wrangler build
|
|
6
|
+
import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
|
|
5
7
|
// @ts-expect-error: Will be resolved by wrangler build
|
|
6
8
|
import { handler as middlewareHandler } from "./middleware/handler.mjs";
|
|
7
9
|
//@ts-expect-error: Will be resolved by wrangler build
|
|
@@ -13,6 +15,10 @@ export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js
|
|
|
13
15
|
export default {
|
|
14
16
|
async fetch(request, env, ctx) {
|
|
15
17
|
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
|
18
|
+
const response = maybeGetSkewProtectionResponse(request);
|
|
19
|
+
if (response) {
|
|
20
|
+
return response;
|
|
21
|
+
}
|
|
16
22
|
const url = new URL(request.url);
|
|
17
23
|
// Serve images in development.
|
|
18
24
|
// Note: "/cdn-cgi/image/..." requests do not reach production workers.
|
|
@@ -29,7 +35,7 @@ export default {
|
|
|
29
35
|
// Fallback for the Next default image loader.
|
|
30
36
|
if (url.pathname === `${globalThis.__NEXT_BASE_PATH__}/_next/image`) {
|
|
31
37
|
const imageUrl = url.searchParams.get("url") ?? "";
|
|
32
|
-
return fetchImage(env.ASSETS, imageUrl);
|
|
38
|
+
return await fetchImage(env.ASSETS, imageUrl, ctx);
|
|
33
39
|
}
|
|
34
40
|
// - `Request`s are handled by the Next server
|
|
35
41
|
const reqOrResp = await middlewareHandler(request, env, ctx);
|
|
@@ -7,11 +7,28 @@ type WranglerOptions = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare function runWrangler(options: BuildOptions, args: string[], wranglerOpts?: WranglerOptions): void;
|
|
9
9
|
export declare function isWranglerTarget(v: string | undefined): v is WranglerTarget;
|
|
10
|
+
/**
|
|
11
|
+
* Returns the value of the flag.
|
|
12
|
+
*
|
|
13
|
+
* The value is retrieved for `<argName> value` or `<argName>=value`.
|
|
14
|
+
*
|
|
15
|
+
* @param args List of args
|
|
16
|
+
* @param argName The arg name with leading dashes, i.e. `--env` or `-e`
|
|
17
|
+
* @returns The value or undefined when not found
|
|
18
|
+
*/
|
|
19
|
+
export declare function getFlagValue(args: string[], ...argNames: string[]): string | undefined;
|
|
10
20
|
/**
|
|
11
21
|
* Find the value of the environment flag (`--env` / `-e`) used by Wrangler.
|
|
12
22
|
*
|
|
13
23
|
* @param args - CLI arguments.
|
|
14
|
-
* @returns Value of the environment flag
|
|
24
|
+
* @returns Value of the environment flag or undefined when not found
|
|
15
25
|
*/
|
|
16
26
|
export declare function getWranglerEnvironmentFlag(args: string[]): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Find the value of the config flag (`--config` / `-c`) used by Wrangler.
|
|
29
|
+
*
|
|
30
|
+
* @param args - CLI arguments.
|
|
31
|
+
* @returns Value of the config flag or undefined when not found
|
|
32
|
+
*/
|
|
33
|
+
export declare function getWranglerConfigFlag(args: string[]): string | undefined;
|
|
17
34
|
export {};
|
|
@@ -62,22 +62,49 @@ export function runWrangler(options, args, wranglerOpts = {}) {
|
|
|
62
62
|
export function isWranglerTarget(v) {
|
|
63
63
|
return !!v && ["local", "remote"].includes(v);
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the value of the flag.
|
|
67
|
+
*
|
|
68
|
+
* The value is retrieved for `<argName> value` or `<argName>=value`.
|
|
69
|
+
*
|
|
70
|
+
* @param args List of args
|
|
71
|
+
* @param argName The arg name with leading dashes, i.e. `--env` or `-e`
|
|
72
|
+
* @returns The value or undefined when not found
|
|
73
|
+
*/
|
|
74
|
+
export function getFlagValue(args, ...argNames) {
|
|
75
|
+
if (argNames.some((name) => !name.startsWith("-"))) {
|
|
76
|
+
// Names should start with "-" or "--"
|
|
77
|
+
throw new Error(`Invalid arg names: ${argNames}`);
|
|
78
|
+
}
|
|
79
|
+
for (const argName of argNames) {
|
|
80
|
+
for (let i = 0; i <= args.length; i++) {
|
|
81
|
+
const arg = args[i];
|
|
82
|
+
if (!arg)
|
|
83
|
+
continue;
|
|
84
|
+
if (arg === argName) {
|
|
85
|
+
return args[i + 1];
|
|
86
|
+
}
|
|
87
|
+
if (arg.startsWith(argName)) {
|
|
88
|
+
return arg.split("=")[1];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
65
93
|
/**
|
|
66
94
|
* Find the value of the environment flag (`--env` / `-e`) used by Wrangler.
|
|
67
95
|
*
|
|
68
96
|
* @param args - CLI arguments.
|
|
69
|
-
* @returns Value of the environment flag
|
|
97
|
+
* @returns Value of the environment flag or undefined when not found
|
|
70
98
|
*/
|
|
71
99
|
export function getWranglerEnvironmentFlag(args) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
100
|
+
return getFlagValue(args, "--env", "-e");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Find the value of the config flag (`--config` / `-c`) used by Wrangler.
|
|
104
|
+
*
|
|
105
|
+
* @param args - CLI arguments.
|
|
106
|
+
* @returns Value of the config flag or undefined when not found
|
|
107
|
+
*/
|
|
108
|
+
export function getWranglerConfigFlag(args) {
|
|
109
|
+
return getFlagValue(args, "--config", "-c");
|
|
83
110
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennextjs/cloudflare",
|
|
3
3
|
"description": "Cloudflare builder for next apps",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@dotenvx/dotenvx": "1.31.0",
|
|
46
46
|
"@opennextjs/aws": "3.7.0",
|
|
47
|
+
"cloudflare": "^4.4.1",
|
|
47
48
|
"enquirer": "^2.4.1",
|
|
48
49
|
"glob": "^11.0.0",
|
|
49
50
|
"ts-tqdm": "^0.8.6"
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
"vitest": "^2.1.1"
|
|
72
73
|
},
|
|
73
74
|
"peerDependencies": {
|
|
74
|
-
"wrangler": "^4.
|
|
75
|
+
"wrangler": "^4.23.0"
|
|
75
76
|
},
|
|
76
77
|
"scripts": {
|
|
77
78
|
"clean": "rimraf dist",
|