Skip to content

Commit

Permalink
[pages] Update pages functions build to include config (#5353)
Browse files Browse the repository at this point in the history
  • Loading branch information
penalosa committed Mar 25, 2024
1 parent 7d160c7 commit 3be826f
Show file tree
Hide file tree
Showing 5 changed files with 371 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-pears-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": minor
---

feat: Updates `wrangler pages functions build` to support using configuration from `wrangler.toml` in the generated output.
234 changes: 234 additions & 0 deletions packages/wrangler/src/__tests__/pages/functions-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
readFileSync,
writeFileSync,
} from "node:fs";
import dedent from "ts-dedent";
import { endEventLoop } from "../helpers/end-event-loop";
import { mockConsoleMethods } from "../helpers/mock-console";
import { runInTempDir } from "../helpers/run-in-tmp";
Expand Down Expand Up @@ -532,3 +533,236 @@ export const cat = "dog";`
expect(std.err).toMatchInlineSnapshot(`""`);
});
});

describe("functions build w/ config", () => {
const std = mockConsoleMethods();

runInTempDir();
const originalEnv = process.env;

afterEach(async () => {
process.env = originalEnv;
// Force a tick to ensure that all promises resolve
await endEventLoop();
});

beforeEach(() => {
// Write an example wrangler.toml file with a _lot_ of config
writeFileSync(
"wrangler.toml",
dedent`
name = "project-name"
pages_build_output_dir = "dist-test"
compatibility_date = "2023-02-14"
placement = { mode = "smart" }
limits = { cpu_ms = 50 }
[vars]
TEST_JSON_PREVIEW = """
{
json: "value"
}"""
TEST_PLAINTEXT_PREVIEW = "PLAINTEXT"
[[kv_namespaces]]
id = "kv-id"
binding = "KV_PREVIEW"
[[kv_namespaces]]
id = "kv-id"
binding = "KV_PREVIEW2"
[[durable_objects.bindings]]
name = "DO_PREVIEW"
class_name = "some-class-do-id"
script_name = "some-script-do-id"
environment = "some-environment-do-id"
[[durable_objects.bindings]]
name = "DO_PREVIEW2"
class_name = "some-class-do-id"
script_name = "some-script-do-id"
environment = "some-environment-do-id"
[[durable_objects.bindings]]
name = "DO_PREVIEW3"
class_name = "do-class"
script_name = "do-s"
environment = "do-e"
[[d1_databases]]
database_id = "d1-id"
binding = "D1_PREVIEW"
database_name = "D1_PREVIEW"
[[d1_databases]]
database_id = "d1-id"
binding = "D1_PREVIEW2"
database_name = "D1_PREVIEW2"
[[r2_buckets]]
bucket_name = "r2-name"
binding = "R2_PREVIEW"
[[r2_buckets]]
bucket_name = "r2-name"
binding = "R2_PREVIEW2"
[[services]]
binding = "SERVICE_PREVIEW"
service = "service"
environment = "production"
[[services]]
binding = "SERVICE_PREVIEW2"
service = "service"
environment = "production"
[[queues.producers]]
binding = "QUEUE_PREVIEW"
queue = "q-id"
[[queues.producers]]
binding = "QUEUE_PREVIEW2"
queue = "q-id"
[[analytics_engine_datasets]]
binding = "AE_PREVIEW"
dataset = "data"
[[analytics_engine_datasets]]
binding = "AE_PREVIEW2"
dataset = "data"
[ai]
binding = "AI_PREVIEW"
[env.production]
compatibility_date = "2024-02-14"
[env.production.vars]
TEST_JSON = """
{
json: "value"
}"""
TEST_PLAINTEXT = "PLAINTEXT"
[[env.production.kv_namespaces]]
id = "kv-id"
binding = "KV"
[[env.production.durable_objects.bindings]]
name = "DO"
class_name = "some-class-do-id"
script_name = "some-script-do-id"
environment = "some-environment-do-id"
[[env.production.d1_databases]]
database_id = "d1-id"
binding = "D1"
database_name = "D1"
[[env.production.r2_buckets]]
bucket_name = "r2-name"
binding = "R2"
[[env.production.services]]
binding = "SERVICE"
service = "service"
environment = "production"
[[env.production.queues.producers]]
binding = "QUEUE"
queue = "q-id"
[[env.production.analytics_engine_datasets]]
binding = "AE"
dataset = "data"
[env.production.ai]
binding = "AI"`
);
});

it("should include all config in the _worker.bundle metadata", async () => {
/* ---------------------------- */
/* Set up js files */
/* ---------------------------- */
mkdirSync("utils");
writeFileSync(
"utils/meaning-of-life.js",
`
export const MEANING_OF_LIFE = 21;
`
);

/* ---------------------------- */
/* Set up _worker.js */
/* ---------------------------- */
mkdirSync("dist-test");
writeFileSync(
"dist-test/_worker.js",
`
import { MEANING_OF_LIFE } from "./../utils/meaning-of-life.js";
export default {
async fetch(request, env) {
return new Response("Hello from _worker.js. The meaning of life is " + MEANING_OF_LIFE);
},
};`
);

/* --------------------------------- */
/* Run cmd & make assertions */
/* --------------------------------- */
// --build-output-directory is included here to validate that it's value is ignored
await runWrangler(
`pages functions build --build-output-directory public --outfile=_worker.bundle --build-metadata-path build-metadata.json --project-directory .`
);
expect(existsSync("_worker.bundle")).toBe(true);
expect(std.out).toMatchInlineSnapshot(`"✨ Compiled Worker successfully"`);

// some values in workerBundleContents, such as the undici form boundary
// or the file hashes, are randomly generated. Let's replace them
// with static values so we can test the file contents
const workerBundleContents = readFileSync("_worker.bundle", "utf-8");
const workerBundleWithConstantData = replaceRandomWithConstantData(
workerBundleContents,
[
[/------formdata-undici-0.[0-9]*/g, "------formdata-undici-0.test"],
[/functionsWorker-0.[0-9]*.js/g, "functionsWorker-0.test.js"],
]
);

expect(workerBundleWithConstantData).toMatchInlineSnapshot(`
"------formdata-undici-0.test
Content-Disposition: form-data; name=\\"metadata\\"
{\\"main_module\\":\\"functionsWorker-0.test.js\\",\\"bindings\\":[{\\"name\\":\\"TEST_JSON_PREVIEW\\",\\"type\\":\\"plain_text\\",\\"text\\":\\"{\\\\njson: \\\\\\"value\\\\\\"\\\\n}\\"},{\\"name\\":\\"TEST_PLAINTEXT_PREVIEW\\",\\"type\\":\\"plain_text\\",\\"text\\":\\"PLAINTEXT\\"},{\\"name\\":\\"KV_PREVIEW\\",\\"type\\":\\"kv_namespace\\",\\"namespace_id\\":\\"kv-id\\"},{\\"name\\":\\"KV_PREVIEW2\\",\\"type\\":\\"kv_namespace\\",\\"namespace_id\\":\\"kv-id\\"},{\\"name\\":\\"DO_PREVIEW\\",\\"type\\":\\"durable_object_namespace\\",\\"class_name\\":\\"some-class-do-id\\",\\"script_name\\":\\"some-script-do-id\\",\\"environment\\":\\"some-environment-do-id\\"},{\\"name\\":\\"DO_PREVIEW2\\",\\"type\\":\\"durable_object_namespace\\",\\"class_name\\":\\"some-class-do-id\\",\\"script_name\\":\\"some-script-do-id\\",\\"environment\\":\\"some-environment-do-id\\"},{\\"name\\":\\"DO_PREVIEW3\\",\\"type\\":\\"durable_object_namespace\\",\\"class_name\\":\\"do-class\\",\\"script_name\\":\\"do-s\\",\\"environment\\":\\"do-e\\"},{\\"type\\":\\"queue\\",\\"name\\":\\"QUEUE_PREVIEW\\",\\"queue_name\\":\\"q-id\\"},{\\"type\\":\\"queue\\",\\"name\\":\\"QUEUE_PREVIEW2\\",\\"queue_name\\":\\"q-id\\"},{\\"name\\":\\"R2_PREVIEW\\",\\"type\\":\\"r2_bucket\\",\\"bucket_name\\":\\"r2-name\\"},{\\"name\\":\\"R2_PREVIEW2\\",\\"type\\":\\"r2_bucket\\",\\"bucket_name\\":\\"r2-name\\"},{\\"name\\":\\"D1_PREVIEW\\",\\"type\\":\\"d1\\",\\"id\\":\\"d1-id\\"},{\\"name\\":\\"D1_PREVIEW2\\",\\"type\\":\\"d1\\",\\"id\\":\\"d1-id\\"},{\\"name\\":\\"SERVICE_PREVIEW\\",\\"type\\":\\"service\\",\\"service\\":\\"service\\",\\"environment\\":\\"production\\"},{\\"name\\":\\"SERVICE_PREVIEW2\\",\\"type\\":\\"service\\",\\"service\\":\\"service\\",\\"environment\\":\\"production\\"},{\\"name\\":\\"AE_PREVIEW\\",\\"type\\":\\"analytics_engine\\",\\"dataset\\":\\"data\\"},{\\"name\\":\\"AE_PREVIEW2\\",\\"type\\":\\"analytics_engine\\",\\"dataset\\":\\"data\\"},{\\"name\\":\\"AI_PREVIEW\\",\\"type\\":\\"ai\\"}],\\"compatibility_date\\":\\"2023-02-14\\",\\"compatibility_flags\\":[],\\"placement\\":{\\"mode\\":\\"smart\\"},\\"limits\\":{\\"cpu_ms\\":50}}
------formdata-undici-0.test
Content-Disposition: form-data; name=\\"functionsWorker-0.test.js\\"; filename=\\"functionsWorker-0.test.js\\"
Content-Type: application/javascript+module
// ../utils/meaning-of-life.js
var MEANING_OF_LIFE = 21;
// _worker.js
var worker_default = {
async fetch(request, env) {
return new Response(\\"Hello from _worker.js. The meaning of life is \\" + MEANING_OF_LIFE);
}
};
export {
worker_default as default
};
------formdata-undici-0.test--"
`);
const buildMetadataContents = readFileSync("build-metadata.json", "utf-8");
expect(buildMetadataContents).toMatchInlineSnapshot(
`"{\\"wrangler_config_hash\\":\\"49290f05177579eac4442a3cfd403a84429c189fc57e75697605eca07eb49d26\\",\\"build_output_directory\\":\\"dist-test\\"}"`
);

expect(std.err).toMatchInlineSnapshot(`""`);
});
});
90 changes: 49 additions & 41 deletions packages/wrangler/src/api/pages/create-worker-bundle-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { Response } from "undici";
import { createWorkerUploadForm } from "../../deployment-bundle/create-worker-upload-form";
import type { Config } from "../../config";
import type { BundleResult } from "../../deployment-bundle/bundle";
import type { CfWorkerInit } from "../../deployment-bundle/worker";
import type { CfPlacement, CfWorkerInit } from "../../deployment-bundle/worker";
import type { Blob } from "node:buffer";
import type { FormData } from "undici";

Expand All @@ -12,28 +13,27 @@ import type { FormData } from "undici";
* contents
*/
export async function createUploadWorkerBundleContents(
workerBundle: BundleResult
workerBundle: BundleResult,
config: Config | undefined
): Promise<Blob> {
const workerBundleFormData = createWorkerBundleFormData(workerBundle);
const workerBundleFormData = createWorkerBundleFormData(workerBundle, config);
const metadata = JSON.parse(workerBundleFormData.get("metadata") as string);

/**
* Pages doesn't need the metadata bindings returned by
* `createWorkerBundleFormData`. Let's strip them out and return only
* the contents we need
*/
workerBundleFormData.set(
"metadata",
JSON.stringify({ main_module: metadata.main_module })
);
// Remove the empty bindings array if no Pages config has been found
if (config === undefined) {
delete metadata.bindings;
}
workerBundleFormData.set("metadata", JSON.stringify(metadata));

return await new Response(workerBundleFormData).blob();
}

/**
* Creates a `FormData` upload from a `BundleResult`
*/
function createWorkerBundleFormData(workerBundle: BundleResult): FormData {
function createWorkerBundleFormData(
workerBundle: BundleResult,
config: Config | undefined
): FormData {
const mainModule = {
name: path.basename(workerBundle.resolvedEntryPointPath),
filePath: workerBundle.resolvedEntryPointPath,
Expand All @@ -43,43 +43,51 @@ function createWorkerBundleFormData(workerBundle: BundleResult): FormData {
type: workerBundle.bundleType || "esm",
};

const bindings: CfWorkerInit["bindings"] = {
kv_namespaces: config?.kv_namespaces,
vars: config?.vars,
browser: config?.browser,
ai: config?.ai,
durable_objects: config?.durable_objects,
queues: config?.queues.producers?.map((producer) => {
return { binding: producer.binding, queue_name: producer.queue };
}),
r2_buckets: config?.r2_buckets,
d1_databases: config?.d1_databases,
vectorize: config?.vectorize,
hyperdrive: config?.hyperdrive,
services: config?.services,
analytics_engine_datasets: config?.analytics_engine_datasets,
mtls_certificates: config?.mtls_certificates,
send_email: undefined,
wasm_modules: undefined,
text_blobs: undefined,
data_blobs: undefined,
constellation: undefined,
dispatch_namespaces: undefined,
logfwdr: undefined,
unsafe: undefined,
};

// The upload API only accepts an empty string or no specified placement for the "off" mode.
const placement: CfPlacement | undefined =
config?.placement?.mode === "smart" ? { mode: "smart" } : undefined;

const worker: CfWorkerInit = {
name: mainModule.name,
main: mainModule,
modules: workerBundle.modules,
bindings: {
vars: undefined,
kv_namespaces: undefined,
send_email: undefined,
wasm_modules: undefined,
text_blobs: undefined,
browser: undefined,
ai: undefined,
data_blobs: undefined,
durable_objects: undefined,
queues: undefined,
r2_buckets: undefined,
d1_databases: undefined,
vectorize: undefined,
constellation: undefined,
hyperdrive: undefined,
services: undefined,
analytics_engine_datasets: undefined,
dispatch_namespaces: undefined,
mtls_certificates: undefined,
logfwdr: undefined,
unsafe: undefined,
},
bindings,
migrations: undefined,
compatibility_date: undefined,
compatibility_flags: undefined,
compatibility_date: config?.compatibility_date,
compatibility_flags: config?.compatibility_flags,
usage_model: undefined,
keepVars: undefined,
keepSecrets: undefined,
logpush: undefined,
placement: undefined,
placement: placement,
tail_consumers: undefined,
limits: undefined,
limits: config?.limits,
};

return createWorkerUploadForm(worker);
Expand Down

0 comments on commit 3be826f

Please sign in to comment.