Skip to content

Commit

Permalink
feat(wrangler): Add config validation for Pages (#5166)
Browse files Browse the repository at this point in the history
Wrangler proper has a mechanism in place through which
it validates a wrangler.toml file for Workers projects.
As part of adding wrangler.toml support for Pages, we
need to put a similar mechanism in place, to validate
a configuration file against Pages specific requirements.

This commit implements this validation logic for Pages.

Co-authored-by: Carmen Popoviciu <cpopoviciu@cloudflare.com>
  • Loading branch information
CarmenPopoviciu and Carmen Popoviciu committed Mar 18, 2024
1 parent d4c23d2 commit 133a190
Show file tree
Hide file tree
Showing 6 changed files with 557 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/calm-buses-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": minor
---

feat: Implement config file validation for Pages projects

Wrangler proper has a mechanism in place through which it validates a wrangler.toml file for Workers projects. As part of adding wrangler toml support for Pages, we need to put a similar mechanism in place, to validate a configuration file against Pages specific requirements.
224 changes: 224 additions & 0 deletions packages/wrangler/src/__tests__/config-validation-pages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { defaultWranglerConfig } from "../config/config";
import { validatePagesConfig } from "../config/validation-pages";
import type { Config } from "../config";

describe("validatePagesConfig()", () => {
describe("`main` field validation", () => {
it("should error if configuration contains both `pages_build_output_dir` and `main` config fields", () => {
const config = generateConfigurationWithDefaults();
config.pages_build_output_dir = "./public";
config.main = "./src/index.js";

const diagnostics = validatePagesConfig(config, []);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeTruthy();
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Running configuration file validation for Pages:
- Configuration file cannot contain both both \\"main\\" and \\"pages_build_output_dir\\" configuration keys.
Please use \\"main\\" if you are deploying a Worker, or \\"pages_build_output_dir\\" if you are deploying a Pages project.
- Configuration file for Pages projects does not support \\"main\\""
`);
});
});

describe("named environments validation", () => {
it("should pass if no named environments are defined", () => {
const config = generateConfigurationWithDefaults();
config.pages_build_output_dir = "./public";

const diagnostics = validatePagesConfig(config, []);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeFalsy();
});

it("should pass for environments named 'preview' and/or 'production'", () => {
const config = generateConfigurationWithDefaults();
config.pages_build_output_dir = "./public";

let diagnostics = validatePagesConfig(config, ["preview"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeFalsy();

diagnostics = validatePagesConfig(config, ["production"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeFalsy();

diagnostics = validatePagesConfig(config, ["preview", "production"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeFalsy();
});

it("should error for any other named environments", () => {
const config = generateConfigurationWithDefaults();
config.pages_build_output_dir = "./assets";

let diagnostics = validatePagesConfig(config, [
"unsupported-env-name-1",
"unsupported-env-name-2",
]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeTruthy();
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Running configuration file validation for Pages:
- Configuration file contains environment names that are not supported by Pages projects:
unsupported-env-name-1,unsupported-env-name-2.
The supported named-environments for Pages are \\"preview\\" and \\"production\\"."
`);

diagnostics = validatePagesConfig(config, [
"production",
"unsupported-env-name",
]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeTruthy();
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Running configuration file validation for Pages:
- Configuration file contains environment names that are not supported by Pages projects:
unsupported-env-name.
The supported named-environments for Pages are \\"preview\\" and \\"production\\"."
`);
});
});

describe("unsupported fields validation", () => {
it("should pass if configuration contains only Pages-supported configuration fields", () => {
let config = generateConfigurationWithDefaults();
config.pages_build_output_dir = "./dist";

let diagnostics = validatePagesConfig(config, ["preview"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeFalsy();

config = {
...config,
...{
name: "test-pages-project",
compatibility_date: "2024-01-01",
compatibility_flags: ["FLAG1", "FLAG2"],
send_metrics: true,
limits: { cpu_ms: 100 },
placement: { mode: "smart" },
vars: { FOO: "foo" },
durable_objects: {
bindings: [
{ name: "TEST_DO_BINDING", class_name: "TEST_DO_CLASS" },
],
},
kv_namespaces: [{ binding: "TEST_KV_BINDING", id: "1" }],
queues: {
producers: [{ binding: "TEST_QUEUE_BINDING", queue: "test-queue" }],
},
r2_buckets: [
{ binding: "TEST_R2_BINDING", bucket_name: "test-bucket" },
],
d1_databases: [
{
binding: "TEST_D1_BINDING",
database_id: "111",
database_name: "test-db",
},
],
vectorize: [
{ binding: "VECTORIZE_TEST_BINDING", index_name: "test-index" },
],
hyperdrive: [{ binding: "HYPERDRIVE_TEST_BINDING", id: "222" }],
services: [
{ binding: "TEST_SERVICE_BINDING", service: "test-worker" },
],
analytics_engine_datasets: [
{ binding: "TEST_AED_BINDING", dataset: "test-dataset" },
],
ai: { binding: "TEST_AI_BINDING" },
dev: {
ip: "127.0.0.0",
port: 1234,
inspector_port: 5678,
local_protocol: "https",
upstream_protocol: "https",
host: "test-host",
},
},
};

diagnostics = validatePagesConfig(config, ["production"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeFalsy();
});

it("should fail if configuration contains any fields that are not supported by Pages projects", () => {
const defaultConfig = generateConfigurationWithDefaults();
defaultConfig.pages_build_output_dir = "./public";

// test with top-level only config fields
let config: Config = {
...defaultConfig,
...{
wasm_modules: {
MODULE_1: "testModule.mjs",
},
text_blobs: {
BLOB_2: "readme.md",
},
},
};
let diagnostics = validatePagesConfig(config, ["preview"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeTruthy();
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Running configuration file validation for Pages:
- Configuration file for Pages projects does not support \\"wasm_modules\\"
- Configuration file for Pages projects does not support \\"text_blobs\\""
`);

// test with inheritable environment config fields
config = {
...defaultConfig,
...{
triggers: { crons: ["cron1", "cron2"] },
usage_model: "bundled",
build: {
command: "npm run build",
},
node_compat: true,
},
};
diagnostics = validatePagesConfig(config, ["production"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeTruthy();
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Running configuration file validation for Pages:
- Configuration file for Pages projects does not support \\"triggers\\"
- Configuration file for Pages projects does not support \\"usage_model\\"
- Configuration file for Pages projects does not support \\"build\\"
- Configuration file for Pages projects does not support \\"node_compat\\""
`);

// test with non-inheritable environment config fields
// (incl. `queues.consumers`)
config = {
...defaultConfig,
...{
queues: {
producers: [
{ queue: "test-producer", binding: "QUEUE_TEST_BINDING" },
],
consumers: [{ queue: "test-consumer" }],
},
cloudchamber: { vcpu: 100, memory: "2GB" },
},
};
diagnostics = validatePagesConfig(config, ["preview"]);
expect(diagnostics.hasWarnings()).toBeFalsy();
expect(diagnostics.hasErrors()).toBeTruthy();
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Running configuration file validation for Pages:
- Configuration file for Pages projects does not support \\"queues.consumers\\"
- Configuration file for Pages projects does not support \\"cloudchamber\\""
`);
});
});
});

function generateConfigurationWithDefaults() {
return { ...defaultWranglerConfig };
}
110 changes: 109 additions & 1 deletion packages/wrangler/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ import type { CamelCaseKey } from "yargs";
*
* - 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.toml 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 = ConfigFields<DevConfig> & Environment;
export type Config = ConfigFields<DevConfig> & PagesConfigFields & Environment;

export type RawConfig = Partial<ConfigFields<RawDevConfig>> &
PagesConfigFields &
RawEnvironment &
DeprecatedConfigFields &
EnvironmentMap;
Expand Down Expand Up @@ -183,6 +185,18 @@ export interface ConfigFields<Dev extends RawDevConfig> {
keep_vars?: boolean;
}

// Pages-specific configuration fields
export interface PagesConfigFields {
/**
* The directory of static assets to serve.
*
* The presence of this field in `wrangler.toml` 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,
Expand Down Expand Up @@ -279,3 +293,97 @@ interface EnvironmentMap {
export type OnlyCamelCase<T = Record<string, never>> = {
[key in keyof T as CamelCaseKey<key>]: T[key];
};

export const defaultWranglerConfig: Config = {
/*====================================================*/
/* Fields supported by both Workers & Pages */
/*====================================================*/
/* TOP-LEVEL ONLY FIELDS */
pages_build_output_dir: undefined,
send_metrics: 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
local_protocol: "http",
upstream_protocol: "http",
host: undefined,
},

/** 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: [],
hyperdrive: [],
services: [],
analytics_engine_datasets: [],
ai: undefined,

/*====================================================*/
/* Fields supported by Workers only */
/*====================================================*/
/* TOP-LEVEL ONLY FIELDS */
configPath: undefined,
legacy_env: true,
migrations: [],
site: undefined,
assets: undefined,
wasm_modules: undefined,
text_blobs: undefined,
data_blobs: undefined,
keep_vars: undefined,

/** INHERITABLE ENVIRONMENT FIELDS **/
account_id: undefined,
main: undefined,
find_additional_modules: undefined,
preserve_file_names: undefined,
base_dir: undefined,
workers_dev: undefined,
route: undefined,
routes: undefined,
tsconfig: undefined,
jsx_factory: "React.createElement",
jsx_fragment: "React.Fragment",
triggers: {
crons: [],
},
usage_model: undefined,
rules: [],
build: {},
no_bundle: undefined,
minify: undefined,
node_compat: undefined,
dispatch_namespaces: [],
first_party_worker: undefined,
zone_id: undefined,
logfwdr: { bindings: [] },
logpush: undefined,

/** NON-INHERITABLE ENVIRONMENT FIELDS **/
define: {},
cloudchamber: {},
send_email: [],
constellation: [],
browser: undefined,
unsafe: {
bindings: undefined,
metadata: undefined,
},
mtls_certificates: [],
tail_consumers: undefined,
};

0 comments on commit 133a190

Please sign in to comment.