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
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:
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user