Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/brave-lights-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"create-cloudflare": minor
---

Add placeholder substitution in wrangler config files.

When c3 updates the config file:

- The value `"<WORKER_NAME>"` is replaced with the worker name.
- The value `"<COMPATIBILITY_DATE>"` is replaced with the latest worked compatibility date.
8 changes: 4 additions & 4 deletions packages/create-cloudflare/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import TOML from "smol-toml";
import { isInsideGitRepo } from "./git";
import { chooseAccount, wranglerLogin } from "./wrangler/accounts";
import {
readWranglerJson,
readWranglerJsonOrJsonc,
readWranglerToml,
wranglerJsonExists,
wranglerJsonOrJsoncExists,
} from "./wrangler/config";
import type { C3Context } from "types";

Expand Down Expand Up @@ -81,8 +81,8 @@ const isDeployable = async (ctx: C3Context) => {
};

const readWranglerConfig = (ctx: C3Context) => {
if (wranglerJsonExists(ctx)) {
return readWranglerJson(ctx);
if (wranglerJsonOrJsoncExists(ctx)) {
return readWranglerJsonOrJsonc(ctx);
}
const wranglerTomlStr = readWranglerToml(ctx);
return TOML.parse(wranglerTomlStr.replace(/\r\n/g, "\n"));
Expand Down
29 changes: 29 additions & 0 deletions packages/create-cloudflare/src/helpers/__tests__/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ describe("json helpers", () => {
expect(mockReadFile).toHaveBeenCalledWith("/path/to/file.json");
expect(result).toEqual({ name: "test" });
});

test("using a reviver function", () => {
mockReadFile.mockReturnValue(
JSON.stringify({
name: "test",
rootValue: "<REPLACE_ME>",
keep: "<DO_NOT_REPLACE_ME>",
nested: {
value: "<REPLACE_ME>",
list: [["<REPLACE_ME>"], "<DO_NOT_REPLACE_ME>"],
},
}),
);

const result = readJSONWithComments(
"/path/to/file.json",
(_key, value) => (value === "<REPLACE_ME>" ? "REPLACED" : value),
);
expect(mockReadFile).toHaveBeenCalledWith("/path/to/file.json");
expect(result).toEqual({
name: "test",
rootValue: "REPLACED",
keep: "<DO_NOT_REPLACE_ME>",
nested: {
value: "REPLACED",
list: [["REPLACED"], "<DO_NOT_REPLACE_ME>"],
},
});
});
});

describe("writeJSONWithComments", () => {
Expand Down
9 changes: 7 additions & 2 deletions packages/create-cloudflare/src/helpers/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ import type {
CommentObject,
CommentSymbol,
CommentToken,
Reviver,
} from "comment-json";

/**
* Reads a JSON file and preserves comments.
* @param jsonFilePath - The path to the JSON file.
* @param reviver A function that transforms the results. This function is called for each member of the object.
* @returns The parsed JSON object with comments.
*/
export function readJSONWithComments(jsonFilePath: string): CommentObject {
export function readJSONWithComments(
jsonFilePath: string,
reviver?: Reviver,
): CommentObject {
const jsonString = readFile(jsonFilePath);
const jsonObject = parse(jsonString) as unknown as CommentObject;
const jsonObject = parse(jsonString, reviver) as unknown as CommentObject;
return jsonObject;
}

Expand Down
8 changes: 4 additions & 4 deletions packages/create-cloudflare/src/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { installPackages } from "helpers/packages";
import * as jsonc from "jsonc-parser";
import TOML from "smol-toml";
import {
readWranglerJson,
readWranglerJsonOrJsonc,
readWranglerToml,
wranglerJsonExists,
wranglerJsonOrJsoncExists,
wranglerTomlExists,
} from "./wrangler/config";
import type { C3Context, PackageJson } from "types";
Expand Down Expand Up @@ -62,8 +62,8 @@ async function generateWorkersTypes(ctx: C3Context, npm: string) {

const maybeInstallNodeTypes = async (ctx: C3Context, npm: string) => {
let parsedConfig: Record<string, unknown> = {};
if (wranglerJsonExists(ctx)) {
parsedConfig = readWranglerJson(ctx);
if (wranglerJsonOrJsoncExists(ctx)) {
parsedConfig = readWranglerJsonOrJsonc(ctx);
} else if (wranglerTomlExists(ctx)) {
const wranglerTomlStr = readWranglerToml(ctx);
parsedConfig = TOML.parse(wranglerTomlStr);
Expand Down
160 changes: 158 additions & 2 deletions packages/create-cloudflare/src/wrangler/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ describe("update wrangler config", () => {
);
});

test("placeholder replacement", async () => {
test("placeholder replacement `<TBD>`", async () => {
const toml = [
`name = "<TBD>"`,
`main = "src/index.ts"`,
`compatibility_date = "<TBD>"`,
`[[services]]`,
`binding = "SELF_SERVICE"`,
`service = "<WORKER_NAME>"`,
].join("\n");
vi.mocked(readFile).mockReturnValue(toml);

Expand All @@ -49,6 +52,10 @@ describe("update wrangler config", () => {
main = "src/index.ts"
compatibility_date = "2024-01-17"

[[services]]
binding = "SELF_SERVICE"
service = "test"

[observability]
enabled = true

Expand Down Expand Up @@ -87,14 +94,157 @@ describe("update wrangler config", () => {
`);
});

test("placeholder replacement (json)", async () => {
test("placeholder replacement", async () => {
const toml = [
`name = "<WORKER_NAME>"`,
`main = "src/index.ts"`,
`compatibility_date = "<COMPATIBILITY_DATE>"`,
`[[services]]`,
`binding = "SELF_SERVICE"`,
`service = "<WORKER_NAME>"`,
].join("\n");
vi.mocked(readFile).mockReturnValue(toml);

await updateWranglerConfig(ctx);

const newToml = vi.mocked(writeFile).mock.calls[0][1];
expect(newToml).toMatchInlineSnapshot(`
"#:schema node_modules/wrangler/config-schema.json
# For more details on how to configure Wrangler, refer to:
# https://developers.cloudflare.com/workers/wrangler/configuration/
name = "test"
main = "src/index.ts"
compatibility_date = "2024-01-17"

[[services]]
binding = "SELF_SERVICE"
service = "test"

[observability]
enabled = true

# Smart Placement
# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
# [placement]
# mode = "smart"

###
# Bindings
# Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
# databases, object storage, AI inference, real-time communication and more.
# https://developers.cloudflare.com/workers/runtime-apis/bindings/
###

# Environment Variables
# https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
# [vars]
# MY_VARIABLE = "production_value"

# Note: Use secrets to store sensitive data.
# https://developers.cloudflare.com/workers/configuration/secrets/

# Static Assets
# https://developers.cloudflare.com/workers/static-assets/binding/
# [assets]
# directory = "./public/"
# binding = "ASSETS"

# Service Bindings (communicate between multiple Workers)
# https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
# [[services]]
# binding = "MY_SERVICE"
# service = "my-service"
"
`);
});

test("placeholder replacement `<TBD>` (json)", async () => {
vi.mocked(existsSync).mockImplementationOnce((f) =>
(f as string).endsWith(".json"),
);
const json = JSON.stringify({
name: "<TBD>",
main: "src/index.ts",
compatibility_date: "<TBD>",
services: [
{
binding: "SELF_SERVICE",
service: "<WORKER_NAME>",
},
],
});
vi.mocked(readFile).mockReturnValueOnce(json);

await updateWranglerConfig(ctx);

const newConfig = vi.mocked(writeFile).mock.calls[0][1];
expect(newConfig).toMatchInlineSnapshot(`
"/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "test",
"main": "src/index.ts",
"compatibility_date": "2024-01-17",
"services": [
{
"binding": "SELF_SERVICE",
"service": "test"
}
],
"observability": {
"enabled": true
}
/**
* Smart Placement
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
*/
// "placement": { "mode": "smart" }
/**
* Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
*/
// "vars": { "MY_VARIABLE": "production_value" }
/**
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/
*/
/**
* Static Assets
* https://developers.cloudflare.com/workers/static-assets/binding/
*/
// "assets": { "directory": "./public/", "binding": "ASSETS" }
/**
* Service Bindings (communicate between multiple Workers)
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
*/
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
}"
`);
});

test("placeholder replacement (json)", async () => {
vi.mocked(existsSync).mockImplementationOnce((f) =>
(f as string).endsWith(".json"),
);
const json = JSON.stringify({
name: "<WORKER_NAME>",
main: "src/index.ts",
compatibility_date: "<COMPATIBILITY_DATE>",
services: [
{
binding: "SELF_SERVICE",
service: "<WORKER_NAME>",
},
],
});
vi.mocked(readFile).mockReturnValueOnce(json);

Expand All @@ -111,6 +261,12 @@ describe("update wrangler config", () => {
"name": "test",
"main": "src/index.ts",
"compatibility_date": "2024-01-17",
"services": [
{
"binding": "SELF_SERVICE",
"service": "test"
}
],
"observability": {
"enabled": true
}
Expand Down
Loading
Loading