chore: import upstream snapshot with attribution
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:11 +08:00
commit 70bf21e064
5213 changed files with 731895 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
# Configuration validation
The files in this directory define and validate the configuration that is read from a Wrangler configuration file.
The configuration for a Worker is complicated since we can define different "environments", and each environment can have its own configuration.
There is a default ("top-level") environment and then named environments that provide environment specific configuration.
This is further complicated by the fact that there are three kinds of environment configuration:
- **non-overridable**: these values are defined once in the top-level configuration, apply to all environments and cannot be overridden by an environment.
- **inheritable**: these values can be defined at the top-level but can also be overridden by environment specific values.
Named environments do not need to provide their own values, in which case they inherit the value from the top-level.
- **non-inheritable**: these values must be explicitly defined in each environment if they are defined at the top-level.
Named environments do not inherit such configuration and must provide their own values.
All configuration values in Wrangler configuration are optional and will receive a default value if not defined.
## Types
### Environment
The fields that can be defined within the `env` containers are defined in the [`Environment`](./environment.ts) type.
This includes the `EnvironmentInheritable` and `EnvironmentNonInheritable` fields.
### Config
The "non-overridable" types are defined in the [`ConfigFields`](./config.ts) type.
The `Config` type is the overall configuration, which consists of the `ConfigFields` and also an `Environment`.
In this case the `Environment`, here, corresponds to the "currently active" environment. This is specified by the `--env` command line argument.
If there is no argument passed then the currently active environment is the "top-level" environment.
The fields in `Config` and `Environment` are not generally optional and so you can expect they have been filled with suitable inherited or default values.
These types should be used when you are working with fields that should be passed to commands.
### RawConfig
The `RawConfig` type is a version of `Config`, where all the fields are optional.
The `RawConfig` type includes `DeprecatedConfigFields` and `EnvironmentMap`.
It also extends the `RawEnvironment` type, which is a version of `Environment` where all the fields are optional.
These optional fields map to the actual fields found in the Wrangler configuration file.
These types should be used when you are working with raw configuration that is read or will be written to a Wrangler configuration file.
## Validation
Validation is triggered by passing a `RawConfig` object, and the active environment name, to the `normalizeAndValidateConfig()` function.
This function will return:
- a `Config` object, where all the fields have suitable valid values
- a `Diagnostics` object, which contains any errors or warnings from the validation process
The field values may have been parsed directly from the `RawConfig`, inherited into a named environment from the top-level environment, or given a default value.
Generally, if there are any warnings they should be presented to the user via `logger.warn()` messages,
and if there are any errors then an `Error` should be thrown describing these errors.
The `Diagnostics` object is hierarchical: each `Diagnostics` instance can contain a collection of child `Diagnostics` instance.
When checking for or rendering warnings and errors, the `Diagnostics` class will automatically traverse down to all its children.
## Usage
The [high level API](./index.ts) for configuration processing consists of the `findWranglerToml()` and `readConfig()` functions.
### readConfig()
The `readConfig()` function will find the nearest Wrangler configuration file, load and parse it, then validate and normalize the values into a `Config` object.
Note that you should pass the current active environment name in as a parameter. The resulting `Config` object will contain only the fields appropriate to that environment.
If there are validation errors then it will throw a suitable error.
## Changing configuration
### Add a new configuration field
When a new field needs to be added to the Wrangler configuration you will need to add to the types and validation code in this directory.
Here are some steps that you should consider when doing this:
- add the new field to one of the interface:
- if the field is not overridable in an environment then add it to the `ConfigFields` interface.
- if the field can be inherited and overridden in a named environment then add it to the `EnvironmentInheritable` interface.
- if the field cannot be inherited and must be specified in each named environment then add it to the `EnvironmentNonInheritable` interface.
- if the field is experimental then add a call to the `experimental()` function:
- if the field is now in the `ConfigDeprecated` interface then add the call to the `normalizeAndValidateConfig()` function.
- if the field is now in the `EnvironmentDeprecated` interface then add the call to the `normalizeAndValidateEnvironment()` function.
- add validation and normalization to the interface
- if the field is in `ConfigFields` then add validation calls to `normalizeAndValidateConfig()` and assign the normalized value to the appropriate property in the `config` object.
- if the field is in `EnvironmentInheritable` then call `inheritable()` in `normalizeAndValidateEnvironment()` and assign the normalized value to the appropriate property in the `environment` object.
- if the field is in `EnvironmentNonInheritable` then call `notInheritable()` in `normalizeAndValidateEnvironment()` and assign the normalized value to the appropriate property in the `environment` object.
- update the tests in `configuration.test.ts`
- add to the `"should use defaults for empty configuration"` test to prove the correct default value is assigned
- if the field is in `ConfigFields` add tests to the `"top-level non-environment configuration"` block to check the validation and normalization of values
- if the field is in `EnvironmentInheritable` or `EnvironmentNonInheritable`
- add tests to the `"top-level environment configuration"` block to check the validation and normalization of values
- add tests to the `"named environment configuration"` block to check the inheritance of values
### Remove a configuration field
We should not just remove a field from use, since users would not know that this field is no longer needed, nor how to migrate to any new usage.
Instead we should first deprecate it, and then then remove it in a future major release.
- move the field to a deprecation interface:
- if the field was originally in `ConfigFields` then move it to the `ConfigDeprecated` interface.
- if the field was originally in either `EnvironmentInheritable` or `EnvironmentNonInheritable` then move it to the `EnvironmentDeprecatedInterface`.
- remove the validation for the field
- the TypeScript compiler should indicate where there is now an unknown field, so that you can remove its validation and normalization from the code base.
- add a deprecation warning by calling `deprecated()`
- if the field is now in the `ConfigDeprecated` interface then add the call to the `normalizeAndValidateConfig()` function.
- if the field is now in the `EnvironmentDeprecated` interface then add the call to the `normalizeAndValidateEnvironment()` function.
- update the tests in `configuration.test.ts`
- Find tests where the field is mentioned and either remove them (e.g. validation) or update to show the deprecation message.
@@ -0,0 +1,83 @@
import type { Binding } from "../types";
/**
* Local-dev capability of each binding type. Source of truth for
* `pickRemoteBindings()` and `warnOrError()`.
*
* - `local-and-remote`: local simulator; `remote: true` opts into proxying.
* - `local-only`: local simulator only; `remote: true` is a config error.
* - `remote`: no local simulator *yet* — requires explicit `remote: true`.
* Move to `local-and-remote` once a simulator lands.
* - `DO-NOT-USE-this-resource-will-never-have-a-local-simulator`: no local
* simulator, *ever* — fundamentally remote-only. Always auto-routed; user
* is warned about usage charges. Adding here is permanent; prefer any
* other variant if a simulator is plausible.
*/
export type BindingLocalSupport =
| "local-and-remote"
| "local-only"
| "remote"
| "DO-NOT-USE-this-resource-will-never-have-a-local-simulator";
const BINDING_LOCAL_SUPPORT: Record<
Exclude<Binding["type"], `unsafe_${string}`> | "unsafe_hello_world",
BindingLocalSupport
> = {
plain_text: "local-only",
secret_text: "local-only",
json: "local-only",
wasm_module: "local-only",
text_blob: "local-only",
data_blob: "local-only",
version_metadata: "local-only",
inherit: "local-only",
logfwdr: "local-only",
assets: "local-only",
unsafe_hello_world: "local-only",
durable_object_namespace: "local-only",
hyperdrive: "local-only",
fetcher: "local-only",
analytics_engine: "local-only",
secrets_store_secret: "local-only",
ratelimit: "local-only",
worker_loader: "local-only",
kv_namespace: "local-and-remote",
r2_bucket: "local-and-remote",
d1: "local-and-remote",
workflow: "local-and-remote",
browser: "local-and-remote",
images: "local-and-remote",
stream: "local-and-remote",
send_email: "local-and-remote",
pipeline: "local-and-remote",
service: "local-and-remote",
// TODO: Miniflare currently ignores `remote: true` on queues, tracked in #13727.
queue: "local-and-remote",
vectorize: "remote",
mtls_certificate: "remote",
dispatch_namespace: "remote",
// Reach out to the @cloudflare/wrangler team before adding anything here
ai: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
ai_search: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
ai_search_namespace:
"DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
media: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
artifacts: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
flagship: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
vpc_service: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
vpc_network: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
websearch: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
agent_memory: "DO-NOT-USE-this-resource-will-never-have-a-local-simulator",
};
export function getBindingLocalSupport(
type: Binding["type"]
): BindingLocalSupport {
if (type in BINDING_LOCAL_SUPPORT) {
return BINDING_LOCAL_SUPPORT[type as keyof typeof BINDING_LOCAL_SUPPORT];
}
return "local-only";
}
@@ -0,0 +1,183 @@
import { existsSync } from "node:fs";
import path from "node:path";
import * as find from "empathic/find";
import dedent from "ts-dedent";
import { PATH_TO_DEPLOY_CONFIG } from "../constants";
import { UserError } from "../errors";
import { parseJSONC, readFileSync } from "../parse";
import type { ComputedFields, RawConfig, RedirectedRawConfig } from "./config";
export type ResolveConfigPathOptions = {
useRedirectIfAvailable?: boolean;
};
export type ConfigPaths = {
/** Absolute path to the actual configuration being used (possibly redirected from the user's config). */
configPath: string | undefined;
/** Absolute path to the user's configuration, which may not be the same as `configPath` if it was redirected. */
userConfigPath: string | undefined;
/** Absolute path to the deploy config path used */
deployConfigPath: string | undefined;
/** Was a redirected config file read? */
redirected: boolean;
};
/**
* Resolve the path to the configuration file, given the `config` and `script` optional command line arguments.
* `config` takes precedence, then `script`, then we just use the cwd.
*
* Returns an object with two paths: `configPath` and `userConfigPath`. If defined these are absolute file paths.
*/
export function resolveWranglerConfigPath(
{
config,
script,
}: {
config?: string;
script?: string;
},
options: { useRedirectIfAvailable?: boolean }
): ConfigPaths {
if (config !== undefined) {
return {
userConfigPath: config,
configPath: config,
deployConfigPath: undefined,
redirected: false,
};
}
const leafPath = script !== undefined ? path.dirname(script) : process.cwd();
return findWranglerConfig(leafPath, options);
}
/**
* Find the wrangler configuration file by searching up the file-system
* from the current working directory.
*/
export function findWranglerConfig(
referencePath: string = process.cwd(),
{ useRedirectIfAvailable = false } = {}
): ConfigPaths {
const userConfigPath =
find.file(`wrangler.json`, { cwd: referencePath }) ??
find.file(`wrangler.jsonc`, { cwd: referencePath }) ??
find.file(`wrangler.toml`, { cwd: referencePath });
if (!useRedirectIfAvailable) {
return {
userConfigPath,
configPath: userConfigPath,
deployConfigPath: undefined,
redirected: false,
};
}
const { configPath, deployConfigPath, redirected } =
findRedirectedWranglerConfig(referencePath, userConfigPath);
return {
userConfigPath,
configPath,
deployConfigPath,
redirected,
};
}
/**
* Check whether there is a configuration file that indicates that we should redirect the user configuration.
* @param cwd
* @param userConfigPath
* @returns
*/
function findRedirectedWranglerConfig(
cwd: string,
userConfigPath: string | undefined
): {
configPath: string | undefined;
deployConfigPath: string | undefined;
redirected: boolean;
} {
const deployConfigPath = find.file(PATH_TO_DEPLOY_CONFIG, { cwd });
if (deployConfigPath === undefined) {
return { configPath: userConfigPath, deployConfigPath, redirected: false };
}
let redirectedConfigPath: string | undefined;
const deployConfigFile = readFileSync(deployConfigPath);
try {
const deployConfig = parseJSONC(deployConfigFile, deployConfigPath) as {
configPath?: string;
};
redirectedConfigPath =
deployConfig.configPath &&
path.resolve(path.dirname(deployConfigPath), deployConfig.configPath);
} catch (e) {
throw new UserError(
`Failed to parse the deploy configuration file at ${path.relative(".", deployConfigPath)}`,
{ cause: e, telemetryMessage: false }
);
}
if (!redirectedConfigPath) {
throw new UserError(
dedent`
A deploy configuration file was found at "${path.relative(".", deployConfigPath)}".
But this is not valid - the required "configPath" property was not found.
Instead this file contains:
\`\`\`
${deployConfigFile}
\`\`\`
`,
{ telemetryMessage: false }
);
}
if (!existsSync(redirectedConfigPath)) {
throw new UserError(
dedent`
There is a deploy configuration at "${path.relative(".", deployConfigPath)}".
But the redirected configuration path it points to, "${path.relative(".", redirectedConfigPath)}", does not exist.
`,
{ telemetryMessage: false }
);
}
if (userConfigPath) {
if (
path.join(path.dirname(userConfigPath), PATH_TO_DEPLOY_CONFIG) !==
deployConfigPath
) {
throw new UserError(
dedent`
Found both a user configuration file at "${path.relative(".", userConfigPath)}"
and a deploy configuration file at "${path.relative(".", deployConfigPath)}".
But these do not share the same base path so it is not clear which should be used.
`,
{ telemetryMessage: false }
);
}
}
return {
configPath: redirectedConfigPath,
deployConfigPath,
redirected: true,
};
}
export function isRedirectedConfig(
config: Pick<ComputedFields, "configPath" | "userConfigPath">
): boolean {
return (
config.configPath !== undefined &&
config.configPath !== config.userConfigPath
);
}
export function isRedirectedRawConfig(
rawConfig: RawConfig,
configPath: string | undefined,
userConfigPath: string | undefined
): rawConfig is RedirectedRawConfig {
return isRedirectedConfig({ configPath, userConfigPath });
}
+431
View File
@@ -0,0 +1,431 @@
import type {
ContainerEngine,
Environment,
RawEnvironment,
} from "./environment";
/**
* This is the static type definition for the configuration object.
*
* It reflects a normalized and validated version of the configuration that you can write in a Wrangler configuration file,
* and optionally augment with arguments passed directly to wrangler.
*
* For more information about the configuration object, see the
* documentation at https://developers.cloudflare.com/workers/cli-wrangler/configuration
*
* Notes:
*
* - Fields that are only specified in `ConfigFields` and not `Environment` can only appear
* in the top level config and should not appear in any environments.
* - Fields that are specified in `PagesConfigFields` are only relevant for Pages projects
* - All top level fields in config and environments are optional in the Wrangler configuration file.
*
* Legend for the annotations:
*
* - `@breaking`: the deprecation/optionality is a breaking change from Wrangler v1.
* - `@todo`: there's more work to be done (with details attached).
*/
export type Config = ComputedFields &
ConfigFields<DevConfig> &
PagesConfigFields &
Environment;
export type RawConfig = Partial<ConfigFields<RawDevConfig>> &
PagesConfigFields &
RawEnvironment &
EnvironmentMap & { $schema?: string };
export type RedirectedRawConfig = RawConfig & Partial<ComputedFields>;
export interface ComputedFields {
/** The path to the Wrangler configuration file (if any, and possibly redirected from the user Wrangler configuration) used to create this configuration. */
configPath: string | undefined;
/** The path to the user's Wrangler configuration file (if any), which may have been redirected to another file that used to create this configuration. */
userConfigPath: string | undefined;
/**
* The original top level name for the Worker in the raw configuration.
*
* When a raw configuration has been flattened to a single environment the worker name may have been replaced or transformed.
* It can be useful to know what the top-level name was before the flattening.
*/
topLevelName: string | undefined;
/** A list of environment names declared in the raw configuration. */
definedEnvironments: string[] | undefined;
/** The name of the environment being targeted. */
targetEnvironment: string | undefined;
}
export interface ConfigFields<Dev extends RawDevConfig> {
/**
* Whether Wrangler should send usage metrics to Cloudflare for this project.
*
* When defined this will override any user settings.
* Otherwise, Wrangler will use the user's preference.
*/
send_metrics: boolean | undefined;
/**
* Configuration for npm package dependency instrumentation.
*
* Controls whether Wrangler should collect and send npm package dependency
* metadata when deploying or uploading a Worker version.
*
* When `enabled` is set to `false`, Wrangler will not include
* `package_dependencies` in the upload payload. Defaults to enabled when
* not specified.
*
* Note: This is considered build metadata, so managed separately from the
* telemetry one and not disabled when
* `send_metrics`/`WRANGLER_SEND_METRICS` is set to `false`
*/
dependencies_instrumentation:
| {
/** Whether dependency instrumentation is enabled. Defaults to `true`. */
enabled: boolean;
}
| undefined;
/**
* Options to configure the development server that your worker will use.
*
* For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#local-development-settings
*/
dev: Dev;
/**
* The definition of a Worker Site, a feature that lets you upload
* static assets with your Worker.
*
* More details at https://developers.cloudflare.com/workers/platform/sites
*
* For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#workers-sites
*/
site:
| {
/**
* The directory containing your static assets.
*
* It must be a path relative to your Wrangler configuration file.
* Example: bucket = "./public"
*
* If there is a `site` field then it must contain this `bucket` field.
*/
bucket: string;
/**
* The location of your Worker script.
*
* @deprecated DO NOT use this (it's a holdover from Wrangler v1.x). Either use the top level `main` field, or pass the path to your entry file as a command line argument.
* @breaking
*/
"entry-point"?: string;
/**
* An exclusive list of .gitignore-style patterns that match file
* or directory names from your bucket location. Only matched
* items will be uploaded. Example: include = ["upload_dir"]
*
* @optional
* @default []
*/
include?: string[];
/**
* A list of .gitignore-style patterns that match files or
* directories in your bucket that should be excluded from
* uploads. Example: exclude = ["ignore_dir"]
*
* @optional
* @default []
*/
exclude?: string[];
}
| undefined;
/**
* A list of wasm modules that your worker should be bound to. This is
* the "legacy" way of binding to a wasm module. ES module workers should
* do proper module imports.
*/
wasm_modules:
| {
[key: string]: string;
}
| undefined;
/**
* A list of text files that your worker should be bound to. This is
* the "legacy" way of binding to a text file. ES module workers should
* do proper module imports.
*/
text_blobs:
| {
[key: string]: string;
}
| undefined;
/**
* A list of data files that your worker should be bound to. This is
* the "legacy" way of binding to a data file. ES module workers should
* do proper module imports.
*/
data_blobs:
| {
[key: string]: string;
}
| undefined;
/**
* A map of module aliases. Lets you swap out a module for any others.
* Corresponds with esbuild's `alias` config
*
* For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing
*/
alias: { [key: string]: string } | undefined;
/**
* By default, the Wrangler configuration file is the source of truth for your environment configuration, like a terraform file.
*
* If you change your vars in the dashboard, wrangler *will* override/delete them on its next deploy.
*
* If you want to keep your dashboard vars when wrangler deploys, set this field to true.
*
* @default false
* @nonInheritable
*/
keep_vars?: boolean;
}
// Pages-specific configuration fields
interface PagesConfigFields {
/**
* The directory of static assets to serve.
*
* The presence of this field in a Wrangler configuration file indicates a Pages project,
* and will prompt the handling of the configuration file according to the
* Pages-specific validation rules.
*/
pages_build_output_dir?: string;
}
export interface DevConfig {
/**
* IP address for the local dev server to listen on,
*
* @default localhost
*/
ip: string;
/**
* Port for the local dev server to listen on
*
* @default 8787
*/
port: number | undefined;
/**
* Port for the local dev server's inspector to listen on
*
* @default 9229
*/
inspector_port: number | undefined;
/**
* IP address for the local dev server's inspector to listen on
*
* @default 127.0.0.1
*/
inspector_ip: string | undefined;
/**
* Protocol that local wrangler dev server listens to requests on.
*
* @default http
*/
local_protocol: "http" | "https";
/**
* Protocol that wrangler dev forwards requests on
*
* Setting this to `http` is not currently implemented for remote mode.
* See https://github.com/cloudflare/workers-sdk/issues/583
*
* @default https
*/
upstream_protocol: "https" | "http";
/**
* Host to forward requests to, defaults to the host of the first route of project
*/
host: string | undefined;
/**
* When developing, whether to build and connect to containers. This requires a Docker daemon to be running.
* Defaults to `true`.
*
* @default true
*/
enable_containers: boolean;
/**
* Either the Docker unix socket i.e. `unix:///var/run/docker.sock` or a full configuration.
* Note that windows is only supported via WSL at the moment
*/
container_engine: ContainerEngine | undefined;
/**
* Re-generate your worker types when your Wrangler configuration file changes.
*
* @default false
*/
generate_types: boolean;
}
export type RawDevConfig = Partial<DevConfig>;
interface EnvironmentMap {
/**
* The `env` section defines overrides for the configuration for different environments.
*
* All environment fields can be specified at the top level of the config indicating the default environment settings.
*
* - Some fields are inherited and overridable in each environment.
* - But some are not inherited and must be explicitly specified in every environment, if they are specified at the top level.
*
* For more information, see the documentation at https://developers.cloudflare.com/workers/cli-wrangler/configuration#environments
*
* @default {}
*/
env?: {
[envName: string]: RawEnvironment;
};
}
export const defaultWranglerConfig: Config = {
/* COMPUTED_FIELDS */
configPath: undefined,
userConfigPath: undefined,
topLevelName: undefined,
definedEnvironments: undefined,
targetEnvironment: undefined,
/*====================================================*/
/* Fields supported by both Workers & Pages */
/*====================================================*/
/* TOP-LEVEL ONLY FIELDS */
pages_build_output_dir: undefined,
send_metrics: undefined,
dependencies_instrumentation: undefined,
dev: {
ip: process.platform === "win32" ? "127.0.0.1" : "localhost",
port: undefined, // the default of 8787 is set at runtime
inspector_port: undefined, // the default of 9229 is set at runtime
inspector_ip: undefined, // the default of 127.0.0.1 is set at runtime
local_protocol: "http",
upstream_protocol: "http",
host: undefined,
// Note this one is also workers only
enable_containers: true,
container_engine: undefined,
generate_types: false,
},
/** INHERITABLE ENVIRONMENT FIELDS **/
name: undefined,
compatibility_date: undefined,
compatibility_flags: [],
limits: undefined,
placement: undefined,
/** NON-INHERITABLE ENVIRONMENT FIELDS **/
vars: {},
durable_objects: { bindings: [] },
kv_namespaces: [],
queues: {
producers: [],
consumers: [], // WORKERS SUPPORT ONLY!!
},
r2_buckets: [],
d1_databases: [],
vectorize: [],
ai_search_namespaces: [],
ai_search: [],
websearch: undefined,
agent_memory: [],
hyperdrive: [],
workflows: [],
secrets_store_secrets: [],
artifacts: [],
services: [],
analytics_engine_datasets: [],
ai: undefined,
images: undefined,
stream: undefined,
media: undefined,
version_metadata: undefined,
unsafe_hello_world: [],
flagship: [],
ratelimits: [],
worker_loaders: [],
/*====================================================*/
/* Fields supported by Workers only */
/*====================================================*/
/* TOP-LEVEL ONLY FIELDS */
site: undefined,
wasm_modules: undefined,
text_blobs: undefined,
data_blobs: undefined,
keep_vars: undefined,
alias: undefined,
/** INHERITABLE ENVIRONMENT FIELDS **/
account_id: undefined,
main: undefined,
find_additional_modules: undefined,
preserve_file_names: undefined,
base_dir: undefined,
workers_dev: undefined,
preview_urls: undefined,
route: undefined,
routes: undefined,
tsconfig: undefined,
jsx_factory: "React.createElement",
jsx_fragment: "React.Fragment",
migrations: [],
exports: {},
triggers: {
crons: undefined,
},
rules: [],
build: { command: undefined, watch_dir: "./src", cwd: undefined },
no_bundle: undefined,
minify: undefined,
keep_names: undefined,
dispatch_namespaces: [],
first_party_worker: undefined,
logfwdr: { bindings: [] },
logpush: undefined,
upload_source_maps: undefined,
assets: undefined,
observability: { enabled: true },
cache: undefined,
/** The default here is undefined so that we can delegate to the CLOUDFLARE_COMPLIANCE_REGION environment variable. */
compliance_region: undefined,
python_modules: { exclude: ["**/*.pyc"] },
previews: undefined,
/** NON-INHERITABLE ENVIRONMENT FIELDS **/
define: {},
cloudchamber: {},
containers: undefined,
send_email: [],
browser: undefined,
unsafe: {},
mtls_certificates: [],
tail_consumers: undefined,
streaming_tail_consumers: undefined,
pipelines: [],
vpc_services: [],
vpc_networks: [],
};
@@ -0,0 +1,91 @@
/**
* Diagnostic errors and warnings.
*
* The structure is a tree, where each node can contain zero or more errors, warnings and child diagnostics objects.
* You can check whether the overall tree has errors or warnings, and you can render a string representation of the errors or warnings.
*/
export class Diagnostics {
errors: string[] = [];
warnings: string[] = [];
children: Diagnostics[] = [];
/** Set to true when an unexpected/unknown field is encountered during validation. */
hasUnexpectedFields: boolean = false;
/**
* Create a new Diagnostics object.
* @param description A general description of this collection of messages.
*/
constructor(public description: string) {}
/**
* Merge the given `diagnostics` into this as a child.
*/
addChild(diagnostics: Diagnostics): void {
if (diagnostics.hasErrors() || diagnostics.hasWarnings()) {
this.children.push(diagnostics);
}
}
/** Does this or any of its children have errors. */
hasErrors(): boolean {
if (this.errors.length > 0) {
return true;
} else {
return this.children.some((child) => child.hasErrors());
}
}
/** Does this or any of its children have unexpected fields. */
hasUnexpectedFieldsInTree(): boolean {
if (this.hasUnexpectedFields) {
return true;
} else {
return this.children.some((child) => child.hasUnexpectedFieldsInTree());
}
}
/** Render the errors of this and all its children. */
renderErrors(): string {
return this.render("errors");
}
/** Does this or any of its children have warnings. */
hasWarnings(): boolean {
if (this.warnings.length > 0) {
return true;
} else {
return this.children.some((child) => child.hasWarnings());
}
}
/** Render the warnings of this and all its children. */
renderWarnings(): string {
return this.render("warnings");
}
private render(field: "errors" | "warnings"): string {
const hasMethod = field === "errors" ? "hasErrors" : "hasWarnings";
return indentText(
`${this.description}\n` +
// Output all the fields (errors or warnings) at this level
this[field].map((message) => `- ${indentText(message)}`).join("\n") +
// Output all the child diagnostics at the next level
this.children
.map((child) =>
child[hasMethod]() ? "\n- " + child.render(field) : ""
)
.filter((output) => output !== "")
.join("\n")
);
}
}
/** Indent all the but the first line by two spaces. */
function indentText(str: string): string {
return str
.split("\n")
.map((line, index) =>
(index === 0 ? line : ` ${line}`).replace(/^\s*$/, "")
)
.join("\n");
}
@@ -0,0 +1,18 @@
import { partitionExports } from "./exports";
import type { Config } from "./config";
import type { DurableObjectExport } from "./environment";
/**
* Returns a map of exports that are only of type "durable-object".
*/
export function getDurableObjectExports(
exports: Config["exports"] | undefined
): Record<string, DurableObjectExport> {
return partitionExports(exports)["durable-object"];
}
export function hasDurableObjectExports(
exports: Config["exports"] | undefined
): boolean {
return Object.keys(getDurableObjectExports(exports)).length > 0;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
import type {
DurableObjectExport,
Exports,
WorkerEntrypointExport,
} from "./environment";
export type ExportType = Exports[string]["type"];
export interface PartitionedExports {
"durable-object": Record<string, DurableObjectExport>;
worker: Record<string, WorkerEntrypointExport>;
}
export function partitionExports(
exports: Exports | undefined
): PartitionedExports {
const partitioned: PartitionedExports = {
"durable-object": {},
worker: {},
};
if (exports === undefined) {
return partitioned;
}
for (const [name, entry] of Object.entries(exports)) {
partitioned[entry.type][name] = entry;
}
return partitioned;
}
+152
View File
@@ -0,0 +1,152 @@
import TOML from "smol-toml";
import { parseJSONC, parseTOML, readFileSync } from "../parse";
import { resolveWranglerConfigPath } from "./config-helpers";
import type { Config, RawConfig } from "./config";
import type { ResolveConfigPathOptions } from "./config-helpers";
import type { NormalizeAndValidateConfigArgs } from "./validation";
export type {
Config,
ConfigFields,
DevConfig,
RawConfig,
RawDevConfig,
} from "./config";
export type ConfigBindingOptions = Pick<
Config,
| "ai"
| "browser"
| "d1_databases"
| "dispatch_namespaces"
| "durable_objects"
| "queues"
| "r2_buckets"
| "services"
| "kv_namespaces"
| "mtls_certificates"
| "vectorize"
| "workflows"
| "vpc_services"
>;
export type {
CacheOptions,
ConfiguredExport,
ConfigModuleRuleType,
Environment,
PreviewsConfig,
RawEnvironment,
WorkerEntrypointExport,
} from "./environment";
export { partitionExports } from "./exports";
export type { ExportType, PartitionedExports } from "./exports";
export function configFormat(
configPath: string | undefined
): "json" | "jsonc" | "toml" | "none" {
if (configPath?.endsWith("toml")) {
return "toml";
}
if (configPath?.endsWith("jsonc")) {
return "jsonc";
}
if (configPath?.endsWith("json")) {
return "json";
}
return "none";
}
export function configFileName(configPath: string | undefined) {
const format = configFormat(configPath);
switch (format) {
case "toml":
return "wrangler.toml";
case "json":
return "wrangler.json";
case "jsonc":
return "wrangler.jsonc";
default:
return "Wrangler configuration";
}
}
export function formatConfigSnippet(
snippet: RawConfig,
configPath: Config["configPath"],
formatted = true
) {
const format = configFormat(configPath);
if (format === "toml") {
return TOML.stringify(snippet);
} else {
return formatted
? JSON.stringify(snippet, null, 2)
: JSON.stringify(snippet);
}
}
export const sharedResourceCreationArgs = {
"use-remote": {
type: "boolean",
description:
"Use a remote binding when adding the newly created resource to your config",
},
"update-config": {
type: "boolean",
description:
"Automatically update your config file with the newly added resource",
},
binding: {
type: "string",
description: "The binding name of this resource in your Worker",
},
} as const;
export type ReadConfigCommandArgs = NormalizeAndValidateConfigArgs & {
config?: string;
script?: string;
};
export type ReadConfigOptions = ResolveConfigPathOptions & {
hideWarnings?: boolean;
// Used by the Vite plugin
// If set to `true`, the `main` field is not converted to an absolute path
preserveOriginalMain?: boolean;
};
const parseRawConfigFile = (configPath: string): RawConfig => {
if (configPath.endsWith(".toml")) {
return parseTOML(readFileSync(configPath), configPath) as RawConfig;
}
if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
return parseJSONC(readFileSync(configPath), configPath) as RawConfig;
}
return {};
};
export const experimental_readRawConfig = (
args: ReadConfigCommandArgs,
options: ReadConfigOptions = {}
): {
rawConfig: RawConfig;
configPath: string | undefined;
userConfigPath: string | undefined;
deployConfigPath: string | undefined;
redirected: boolean;
} => {
// Load the configuration from disk if available
const { configPath, userConfigPath, deployConfigPath, redirected } =
resolveWranglerConfigPath(args, options);
const rawConfig = parseRawConfigFile(configPath ?? "");
return {
rawConfig,
configPath,
userConfigPath,
deployConfigPath,
redirected,
};
};
@@ -0,0 +1,97 @@
import { writeFileSync } from "node:fs";
import { applyEdits, format, modify } from "jsonc-parser";
import TOML from "smol-toml";
import { parseJSONC, parseTOML, readFileSync } from "../parse";
import type { RawConfig } from "./config";
import type { JSONPath } from "jsonc-parser";
export const experimental_patchConfig = (
configPath: string,
/**
* if you want to add something new, e.g. a binding, you can just provide that {kv_namespace:[{binding:"KV"}]}
* and set isArrayInsertion = true
*
* if you want to edit or delete existing array elements, you have to provide the whole array
* e.g. {kv_namespace:[{binding:"KV", id:"new-id"}, {binding:"KV2", id:"untouched"}]}
* and set isArrayInsertion = false
*/
patch: RawConfig,
isArrayInsertion: boolean = true
) => {
let configString = readFileSync(configPath);
if (configPath.endsWith("toml")) {
// the TOML parser we use does not preserve comments
if (configString.includes("#")) {
throw new PatchConfigError(
"cannot patch .toml config if comments are present"
);
} else {
// for simplicity, use the JSONC editor to make all edits
// toml -> js object -> json string -> edits -> js object -> toml
configString = JSON.stringify(parseTOML(configString));
}
}
const patchPaths: JSONPath[] = [];
getJSONPath(patch, patchPaths, isArrayInsertion);
for (const patchPath of patchPaths) {
const value = patchPath.pop();
const edit = modify(configString, patchPath, value, {
isArrayInsertion,
});
configString = applyEdits(configString, edit);
}
const formatEdit = format(configString, undefined, {});
configString = applyEdits(configString, formatEdit);
if (configPath.endsWith(".toml")) {
configString = TOML.stringify(parseJSONC(configString));
}
writeFileSync(configPath, configString);
return configString;
};
/**
*
* Gets all the JSON paths for a given object by recursing through the object, recording the properties encountered.
* e.g. {a : { b: "c", d: ["e", "f"]}} -> [["a", "b", "c"], ["a", "d", 0], ["a", "d", 1]]
* The jsonc-parser library requires JSON paths for each edit.
* Note the final 'path' segment is the value we want to insert,
* so in the above example,["a", "b"] would be the path and we would insert "c"
*
* If isArrayInsertion = false, when we encounter an array, we use the item index as part of the path and continue
* If isArrayInsertion = false, we stop recursing down and treat the whole array item as the final path segment/value.
*
*/
const getJSONPath = (
obj: RawConfig,
allPaths: JSONPath[],
isArrayInsertion: boolean,
prevPath: JSONPath = []
) => {
for (const [k, v] of Object.entries(obj)) {
const currentPath = [...prevPath, k];
if (Array.isArray(v)) {
v.forEach((x, i) => {
if (isArrayInsertion) {
// makes sure we insert new array items at the end
allPaths.push([...currentPath, -1, x]);
} else if (typeof x === "object" && x !== null) {
getJSONPath(x, allPaths, isArrayInsertion, [...currentPath, i]);
} else {
allPaths.push([...currentPath, i, x]);
}
});
} else if (typeof v === "object" && v !== null) {
getJSONPath(v, allPaths, isArrayInsertion, currentPath);
} else {
allPaths.push([...currentPath, v]);
}
}
};
/**
* Custom error class for config patching errors
*/
export class PatchConfigError extends Error {}
@@ -0,0 +1,685 @@
import type { RawConfig } from "./config";
import type { Diagnostics } from "./diagnostics";
import type { Environment, RawEnvironment } from "./environment";
/**
* Mark a field as deprecated.
*
* This function will add a diagnostics warning if the deprecated field is found in the `rawEnv` (or an error if it's also a breaking deprecation)
* The `fieldPath` is a dot separated property path, e.g. `"build.upload.format"`.
*/
export function deprecated<T extends object>(
diagnostics: Diagnostics,
config: T,
fieldPath: DeepKeyOf<T>,
message: string,
remove: boolean,
title = "Deprecation",
type: "warning" | "error" = "warning"
): void {
const BOLD = "\x1b[1m";
const NORMAL = "\x1b[0m";
const diagnosticMessage = `${BOLD}${title}${NORMAL}: "${fieldPath}":\n${message}`;
const result = unwindPropertyPath(config, fieldPath);
if (result !== undefined && result.field in result.container) {
diagnostics[`${type}s`].push(diagnosticMessage);
if (remove) {
delete (result.container as Record<string, unknown>)[result.field];
}
}
}
/**
* Mark a field as experimental.
*
* This function will add a diagnostics warning if the experimental field is found in the `rawEnv`.
* The `fieldPath` is a dot separated property path, e.g. `"build.upload.format"`.
*/
export function experimental<T extends object>(
diagnostics: Diagnostics,
config: T,
fieldPath: DeepKeyOf<T>
): void {
const result = unwindPropertyPath(config, fieldPath);
if (
result !== undefined &&
result.field in result.container &&
!("WRANGLER_DISABLE_EXPERIMENTAL_WARNING" in process.env)
) {
diagnostics.warnings.push(
`"${fieldPath}" fields are experimental and may change or break at any time.`
);
}
}
/**
* Get an inheritable environment field, after computing and validating its value.
*
* If the field is not defined in the given environment, then fallback to the value from the top-level config,
* and then the `defaultValue`.
*/
export function inheritable<K extends keyof Environment>(
diagnostics: Diagnostics,
topLevelEnv: Environment | undefined,
rawEnv: RawEnvironment,
field: K,
validate: ValidatorFn,
defaultValue: Environment[K],
transformFn: TransformFn<Environment[K]> = (v) => v
): Environment[K] {
validate(diagnostics, field, rawEnv[field], topLevelEnv);
return (
// `rawEnv === topLevelEnv` is a special case where the user has provided an environment name
// but that named environment is not actually defined in the configuration.
// In that case we have reused the topLevelEnv as the rawEnv,
// and so we need to process the `transformFn()` anyway rather than just using the field in the `rawEnv`.
(rawEnv !== topLevelEnv ? (rawEnv[field] as Environment[K]) : undefined) ??
transformFn(topLevelEnv?.[field]) ??
defaultValue
);
}
/**
* Type of function that is used to transform an inheritable environment field.
*/
type TransformFn<T> = (fieldValue: T | undefined) => T | undefined;
/**
* Transform an environment field by appending current environment name to it.
*/
export const appendEnvName =
(envName: string): TransformFn<string | undefined> =>
(fieldValue) =>
fieldValue ? `${fieldValue}-${envName}` : undefined;
/**
* Get a not inheritable environment field, after computing and validating its value.
*
* If the field is not defined in the given environment but it is defined in the top-level config,
* then log a warning and return the `defaultValue`.
*/
export function notInheritable<K extends keyof Environment>(
diagnostics: Diagnostics,
topLevelEnv: Environment | undefined,
rawConfig: RawConfig | undefined,
rawEnv: RawEnvironment,
envName: string,
field: K,
validate: ValidatorFn,
defaultValue: Environment[K]
): Environment[K] {
if (rawEnv[field] !== undefined) {
validate(diagnostics, field, rawEnv[field], topLevelEnv);
} else {
if (rawConfig?.[field] !== undefined) {
diagnostics.warnings.push(
`"${field}" exists at the top level, but not on "env.${envName}".\n` +
`This is not what you probably want, since "${field}" is not inherited by environments.\n` +
`Please add "${field}" to "env.${envName}".`
);
}
}
return (rawEnv[field] as Environment[K]) ?? defaultValue;
}
// Idea taken from https://stackoverflow.com/a/66661477
type DeepKeyOf<T> = (
T extends object
? {
[K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<DeepKeyOf<T[K]>>}`;
}[Exclude<keyof T, symbol>]
: ""
) extends infer D
? Extract<D, string>
: never;
type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
/**
* Return a container object and field name for the last property in a given property path.
*
* For example, given a path of `"build.upload.format"`) and a starting `root` object
* this will return:
*
* ```
* { container: root.build.upload, field: "format" }
* ```
*/
function unwindPropertyPath<T extends object>(
root: T,
path: DeepKeyOf<T>
): { container: object; field: string } | undefined {
let container: object = root;
const parts = (path as string).split(".");
for (let i = 0; i < parts.length - 1; i++) {
if (!hasProperty(container, parts[i])) {
return;
}
container = container[parts[i]];
}
return { container, field: parts[parts.length - 1] };
}
/**
* The type of a function that can be used to validate a configuration field.
*/
export type ValidatorFn = (
diagnostics: Diagnostics,
field: string,
value: unknown,
topLevelEnv: Environment | undefined
) => boolean;
/**
* Validate that the field is a string.
*/
export const isString: ValidatorFn = (diagnostics, field, value) => {
if (value !== undefined && typeof value !== "string") {
diagnostics.errors.push(
`Expected "${field}" to be of type string but got ${JSON.stringify(
value
)}.`
);
return false;
}
return true;
};
/**
* Validate that the `name` field is compliant with EWC constraints.
*/
export const isValidName: ValidatorFn = (diagnostics, field, value) => {
if (
(typeof value === "string" && /^$|^[a-z0-9_][a-z0-9-_]*$/.test(value)) ||
value === undefined
) {
return true;
} else {
diagnostics.errors.push(
`Expected "${field}" to be of type string, alphanumeric and lowercase with dashes only but got ${JSON.stringify(
value
)}.`
);
return false;
}
};
/**
* Validate that the field is a valid ISO-8601 date time string
* see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
* or https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format
*/
export const isValidDateTimeStringFormat = (
diagnostics: Diagnostics,
field: string,
value: string
): boolean => {
let isValid = true;
// en/em dashes are not valid characters in the JS date time string format.
// While they would be caught by the `isNaN(data.getTime())` check below,
// we want to single these use cases out, and throw a more specific error
if (
value.includes("") || // en-dash
value.includes("—") // em-dash
) {
diagnostics.errors.push(
`"${field}" field should use ISO-8601 accepted hyphens (-) rather than en-dashes () or em-dashes (—).`
);
isValid = false;
}
// en/em dashes were already handled above. Let's replace them with hyphens,
// which is a valid date time string format character, and evaluate the
// resulting date time string further. This ensures we validate for hyphens
// only once!
const data = new Date(value.replaceAll(/|—/g, "-"));
if (isNaN(data.getTime())) {
diagnostics.errors.push(
`"${field}" field should be a valid ISO-8601 date (YYYY-MM-DD), but got ${JSON.stringify(value)}.`
);
isValid = false;
}
return isValid;
};
/**
* Validate that the field is an array of strings.
*/
export const isStringArray: ValidatorFn = (diagnostics, field, value) => {
if (
value !== undefined &&
(!Array.isArray(value) || value.some((item) => typeof item !== "string"))
) {
diagnostics.errors.push(
`Expected "${field}" to be of type string array but got ${JSON.stringify(
value
)}.`
);
return false;
}
return true;
};
/**
* Validate that the field is an object containing the given properties.
*/
export const isObjectWith =
(...properties: string[]): ValidatorFn =>
(diagnostics, field, value) => {
if (
value !== undefined &&
(typeof value !== "object" ||
value === null ||
!properties.every((prop) => prop in value))
) {
diagnostics.errors.push(
`Expected "${field}" to be of type object, containing only properties ${properties}, but got ${JSON.stringify(
value
)}.`
);
return false;
}
// it's an object with the field as desired,
// but let's also check for unexpected fields
if (value !== undefined) {
const restFields = Object.keys(value).filter(
(key) => !properties.includes(key)
);
validateAdditionalProperties(diagnostics, field, restFields, []);
}
return true;
};
/**
* Validate that the field value is one of the given choices.
*/
export const isOneOf =
(...choices: unknown[]): ValidatorFn =>
(diagnostics, field, value) => {
if (value !== undefined && !choices.some((choice) => value === choice)) {
diagnostics.errors.push(
`Expected "${field}" field to be one of ${JSON.stringify(
choices
)} but got ${JSON.stringify(value)}.`
);
return false;
}
return true;
};
/**
* Aggregate multiple validator functions
*/
export const all = (...validations: ValidatorFn[]): ValidatorFn => {
return (diagnostics, field, value, config) => {
let passedValidations = true;
for (const validate of validations) {
if (!validate(diagnostics, field, value, config)) {
passedValidations = false;
}
}
return passedValidations;
};
};
/**
* Check that the field is mutually exclusive with a list of other fields.
*
* @param container the container of the fields to check against.
* @param fields the names of the fields to check against.
*/
export const isMutuallyExclusiveWith = <T extends RawEnvironment | RawConfig>(
container: T,
...fields: (keyof T)[]
): ValidatorFn => {
return (diagnostics, field, value) => {
if (value === undefined) {
return true;
}
for (const exclusiveWith of fields) {
if (container[exclusiveWith] !== undefined) {
diagnostics.errors.push(
`Expected exactly one of the following fields ${JSON.stringify([
field,
...fields,
])}.`
);
return false;
}
}
return true;
};
};
/**
* Validate that the field is a boolean.
*/
export const isBoolean: ValidatorFn = (diagnostics, field, value) => {
if (value !== undefined && typeof value !== "boolean") {
diagnostics.errors.push(
`Expected "${field}" to be of type boolean but got ${JSON.stringify(
value
)}.`
);
return false;
}
return true;
};
/**
* Validate that the required field exists and has the expected type.
*/
export const validateRequiredProperty = (
diagnostics: Diagnostics,
container: string,
key: string,
value: unknown,
type: TypeofType,
choices?: unknown[]
): boolean => {
if (container) {
container += ".";
}
if (value === undefined) {
diagnostics.errors.push(`"${container}${key}" is a required field.`);
return false;
} else if (typeof value !== type) {
diagnostics.errors.push(
`Expected "${container}${key}" to be of type ${type} but got ${JSON.stringify(
value
)}.`
);
return false;
} else if (choices) {
if (
!isOneOf(...choices)(diagnostics, `${container}${key}`, value, undefined)
) {
return false;
}
}
return true;
};
/**
* Validate that at least one of the properties in the list is required.
*/
export const validateAtLeastOnePropertyRequired = (
diagnostics: Diagnostics,
container: string,
properties: {
key: string;
value: unknown;
type: TypeofType;
}[]
): boolean => {
const containerPath = container ? `${container}.` : "";
if (properties.every((property) => property.value === undefined)) {
diagnostics.errors.push(
`${properties.map(({ key }) => `"${containerPath}${key}"`).join(" or ")} is required.`
);
return false;
}
const errors = [];
for (const prop of properties) {
if (typeof prop.value === prop.type) {
return true;
}
errors.push(
`Expected "${containerPath}${prop.key}" to be of type ${prop.type} but got ${JSON.stringify(
prop.value
)}.`
);
}
diagnostics.errors.push(...errors);
return false;
};
/**
* Validate that, if the optional field exists, then it has the expected type.
*/
export const validateOptionalProperty = (
diagnostics: Diagnostics,
container: string,
key: string,
value: unknown,
type: TypeofType,
choices?: unknown[]
): boolean => {
if (value !== undefined) {
return validateRequiredProperty(
diagnostics,
container,
key,
value,
type,
choices
);
}
return true;
};
/**
* Validate that the field is an array of elements of the given type.
*/
export const validateTypedArray = (
diagnostics: Diagnostics,
container: string,
value: unknown,
type: TypeofType
): boolean => {
let isValid = true;
if (!Array.isArray(value)) {
diagnostics.errors.push(
`Expected "${container}" to be an array of ${type}s but got ${JSON.stringify(
value
)}`
);
isValid = false;
} else {
for (let i = 0; i < value.length; i++) {
isValid =
validateRequiredProperty(
diagnostics,
container,
`[${i}]`,
value[i],
type
) && isValid;
}
}
return isValid;
};
/**
* Validate that, if the optional field exists, it is an array of elements of the given type.
*/
export const validateOptionalTypedArray = (
diagnostics: Diagnostics,
container: string,
value: unknown,
type: TypeofType
) => {
if (value !== undefined) {
return validateTypedArray(diagnostics, container, value, type);
}
return true;
};
/**
* Returns whether `target` has the required property `property` of type `type`.
*
* @param target the object to test.
* @param property the property name to test.
* @param type the expected type of the property.
* @param choices optional list of allowed values for the property.
* @returns whether `target` has the required property `property` of type `type`, and optionally one of `choices`.
*/
export const isRequiredProperty = <T extends object>(
target: object,
property: keyof T,
type: TypeofType,
choices?: unknown[]
): target is T =>
hasProperty<T>(target, property) &&
typeof target[property] === type &&
(choices === undefined || choices.includes(target[property]));
/**
* Returns whether `target` has the optional property `property` of type `type`.
*
* @param target the object to test.
* @param property the property name to test.
* @param type the expected type of the property.
* @returns whether `target` has the optional property `property` of type `type`.
*/
export const isOptionalProperty = <T extends object>(
target: object,
property: keyof T,
type: TypeofType
): target is T =>
!hasProperty<T>(target, property) || typeof target[property] === type;
/**
* Returns whether `target` has the property `property`.
*
* @param target the object to test.
* @param property the property name to test.
* @returns whether `target` has the property `property`.
*/
export const hasProperty = <T extends object>(
target: object,
property: keyof T
): target is T => property in target;
/**
* Add warning messages about any properties in the given field that are not expected to be there.
*/
export const validateAdditionalProperties = (
diagnostics: Diagnostics,
fieldPath: string,
restProps: Iterable<string>,
knownProps: Iterable<string>
): boolean => {
const restPropSet = new Set(restProps);
for (const knownProp of knownProps) {
restPropSet.delete(knownProp);
}
if (restPropSet.size > 0) {
const fields = Array.from(restPropSet.keys()).map((field) => `"${field}"`);
diagnostics.warnings.push(
`Unexpected fields found in ${fieldPath} field: ${fields}`
);
diagnostics.hasUnexpectedFields = true;
return false;
}
return true;
};
/**
* Get the names of the bindings collection in `value`.
*
* Will return an empty array if it doesn't understand the value
* passed in, so another form of validation should be
* performed externally.
*/
export const getBindingNames = (value: unknown): string[] => {
if (typeof value !== "object" || value === null) {
return [];
}
if (isBindingList(value)) {
return value.bindings.map(({ name }) => name);
} else if (isNamespaceList(value)) {
return value.map(({ binding }) => binding);
} else if (isRecord(value)) {
// browser and AI bindings are single values with a similar shape
// { binding = "name" }
if (value["binding"] !== undefined) {
return [value["binding"] as string];
}
return Object.keys(value).filter((k) => value[k] !== undefined);
} else {
return [];
}
};
const isBindingList = (
value: unknown
): value is {
bindings: {
name: string;
}[];
} =>
isRecord(value) &&
"bindings" in value &&
Array.isArray(value.bindings) &&
value.bindings.every(
(binding) =>
isRecord(binding) && "name" in binding && typeof binding.name === "string"
);
const isNamespaceList = (value: unknown): value is { binding: string }[] =>
Array.isArray(value) &&
value.every(
(entry) =>
isRecord(entry) && "binding" in entry && typeof entry.binding === "string"
);
const isRecord = (
value: unknown
): value is Record<string | number | symbol, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/**
* Ensure that all bindings in an array have unique `name` properties.
*/
export const validateUniqueNameProperty: ValidatorFn = (
diagnostics,
field,
value
) => {
if (Array.isArray(value)) {
const nameCount = new Map<string, number>();
Object.entries(value).forEach(([_, entry]) => {
nameCount.set(entry.name, (nameCount.get(entry.name) ?? 0) + 1);
});
const duplicates = Array.from(nameCount.entries())
.filter(([_, count]) => count > 1)
.map(([name]) => name);
if (duplicates.length > 0) {
const list = duplicates.join('", "');
diagnostics.errors.push(
`"${field}" bindings must have unique "name" values; duplicate(s) found: "${list}"`
);
return false;
}
}
return true;
};
/**
* JavaScript `typeof` operator return values.
*/
export type TypeofType =
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
@@ -0,0 +1,199 @@
/**
* Pages now supports configuration via a Wrangler configuration file. As opposed to
* Workers however, Pages only supports a limited subset of all available
* configuration keys.
*
* This file contains all Wrangler configuration file validation things, specific to
* Pages.
*/
import { FatalError } from "../errors";
import { defaultWranglerConfig } from "./config";
import { Diagnostics } from "./diagnostics";
import { isRequiredProperty } from "./validation-helpers";
import type { Config } from "./config";
const supportedPagesConfigFields = [
"pages_build_output_dir",
"name",
"compatibility_date",
"compatibility_flags",
"send_metrics",
"no_bundle",
"limits",
"placement",
"vars",
"durable_objects",
"kv_namespaces",
"queues", // `producers` ONLY
"r2_buckets",
"d1_databases",
"vectorize",
"hyperdrive",
"services",
"analytics_engine_datasets",
"ai",
"version_metadata",
"dev",
"mtls_certificates",
"browser",
"upload_source_maps",
// normalizeAndValidateConfig() sets these values
"configPath",
"userConfigPath",
"topLevelName",
"definedEnvironments",
"targetEnvironment",
] as const;
export function validatePagesConfig(
config: Config,
envNames: string[],
projectName?: string
): Diagnostics {
// exhaustive check
if (!config.pages_build_output_dir) {
throw new FatalError(
`Attempting to validate Pages configuration file, but "pages_build_output_dir" field was not found.
"pages_build_output_dir" is required for Pages projects.`,
{ telemetryMessage: false }
);
}
const diagnostics = new Diagnostics(
`Running configuration file validation for Pages:`
);
validateMainField(config, diagnostics);
validateProjectName(projectName, diagnostics);
validatePagesEnvironmentNames(envNames, diagnostics);
validateUnsupportedFields(config, diagnostics);
validateDurableObjectBinding(config, diagnostics);
return diagnostics;
}
/**
* Validate that configuration file doesn't specify "main", if
* "pages_build_output_dir" is present
*/
function validateMainField(config: Config, diagnostics: Diagnostics) {
if (config.main !== undefined) {
diagnostics.errors.push(
`Configuration file cannot contain both both "main" and "pages_build_output_dir" configuration keys.\n` +
`Please use "main" if you are deploying a Worker, or "pages_build_output_dir" if you are deploying a Pages project.`
);
}
}
/**
* Validate that "name" field is specified at the top-level
*/
function validateProjectName(
name: string | undefined,
diagnostics: Diagnostics
) {
if (name === undefined || name.trim() === "") {
diagnostics.errors.push(
`Missing top-level field "name" in configuration file.\n` +
`Pages requires the name of your project to be configured at the top-level of your Wrangler configuration file. This is because, in Pages, environments target the same project.`
);
}
}
/**
* Validate that no named-environments other than "preview" and "production"
* were specified in the configuration file for Pages
*/
function validatePagesEnvironmentNames(
envNames: string[],
diagnostics: Diagnostics
) {
if (!envNames?.length) {
return;
}
const unsupportedPagesEnvNames = envNames.filter(
(name) => name !== "preview" && name !== "production"
);
if (unsupportedPagesEnvNames.length > 0) {
diagnostics.errors.push(
`Configuration file contains the following environment names that are not supported by Pages projects:\n` +
`${unsupportedPagesEnvNames.map((name) => `"${name}"`).join()}.\n` +
`The supported named-environments for Pages are "preview" and "production".`
);
}
}
/**
* Check for configuration fields that are not supported by Pages via the
* configuration file
*/
function validateUnsupportedFields(config: Config, diagnostics: Diagnostics) {
const unsupportedFields = new Set(Object.keys(config) as Array<keyof Config>);
for (const field of supportedPagesConfigFields) {
// Pages config supports `queues.producers` only. However, let's skip
// that validation here and keep all diagnostics handling in one place.
// This way we'll avoid breaking the config key logical grouping in
// `supportedPagesConfigFields`, when writing the errors to stdout.
if (field === "queues" && config.queues?.consumers?.length) {
continue;
}
unsupportedFields.delete(field);
}
for (const field of unsupportedFields) {
// check for unsupported fields with default values and exclude if found.
// These were most likely set as part of `normalizeAndValidateConfig()`
// processing, and not via the config file.
if (
config[field] === undefined ||
JSON.stringify(config[field]) ===
JSON.stringify(defaultWranglerConfig[field])
) {
unsupportedFields.delete(field);
}
}
if (unsupportedFields.size > 0) {
const fields = Array.from(unsupportedFields.keys());
fields.forEach((field) => {
if (field === "queues" && config.queues?.consumers?.length) {
diagnostics.errors.push(
`Configuration file for Pages projects does not support "queues.consumers"`
);
} else {
diagnostics.errors.push(
`Configuration file for Pages projects does not support "${field}"`
);
}
});
}
}
/**
* Validate the "script_name" field is specified for [[durable_objects.bindings]]
*
* This is necessary because Pages cannot define/deploy a DO itself today,
* and so this needs to be done with a Worker.
*/
function validateDurableObjectBinding(
config: Config,
diagnostics: Diagnostics
) {
if (config.durable_objects.bindings.length > 0) {
const invalidBindings = config.durable_objects.bindings.filter(
(binding) => !isRequiredProperty(binding, "script_name", "string")
);
if (invalidBindings.length > 0) {
diagnostics.errors.push(
`Durable Objects bindings should specify a "script_name".\n` +
`Pages requires Durable Object bindings to specify the name of the Worker where the Durable Object is defined.`
);
}
}
}
File diff suppressed because it is too large Load Diff