Skip to content

Commit

Permalink
[C3] Improve bindings support in Svelte template (#5027)
Browse files Browse the repository at this point in the history
* Improve bindings support in svelte template

* Improve parsing of tsconfig when updating types entrypoint

* Improve docstring

* Change svelte copyFiles definition to new format

* Fix formatting issues

* Add changeset

* Fixing svelte verifyBuild tests

* Addressing PR feedback

* Fix build-cf-types task for win32
  • Loading branch information
jculvey authored Feb 16, 2024
1 parent ce23ed7 commit a751489
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 76 deletions.
11 changes: 11 additions & 0 deletions .changeset/twelve-moons-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"create-cloudflare": minor
---

feature: Improve bindings support in Svelte template.

C3 will now create Svelte projects with a hook that uses `getPlatformProxy` to proxy bindings in development mode. A `wrangler.toml` file will also be added where bindings can be added to be used in conjunction with `getPlatformProxy`.

Along with this change, projects will use the default vite-based dev command from `create-svelte` instead of using `wrangler pages dev` on build output.

When Typescript is used, the `app.d.ts` will be updated to add type definitions for `cf` and `ctx` to the `Platform` interface from the `@cloudflare/workers-types` package. Types for bindings on `platform.env` can be re-generated with a newly added `build-cf-types` script.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function GET({ platform }) {
const test = platform?.env.TEST;

return new Response(JSON.stringify({ test }), {
headers: {
"Content-Type": "application/json",
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[vars]
TEST = "C3_TEST"
13 changes: 13 additions & 0 deletions packages/create-cloudflare/e2e-tests/frameworks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ const frameworkTests: Record<string, FrameworkTestConfig> = {
route: "/",
expectedText: "SvelteKit app",
},
verifyDev: {
route: "/test",
expectedText: "C3_TEST",
},
verifyBuild: {
outputDir: ".svelte-kit/cloudflare",
script: "build",
route: "/test",
expectedText: "C3_TEST",
},
},
vue: {
testCommitMessage: true,
Expand Down Expand Up @@ -468,6 +478,9 @@ const verifyBuildScript = async (
[pm, "run", script],
{
cwd: projectPath,
env: {
NODE_ENV: "production",
},
},
logStream
);
Expand Down
4 changes: 2 additions & 2 deletions packages/create-cloudflare/e2e-tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export const spawnWithLogging = (
) => {
const [cmd, ...argv] = args;

logStream.write(`\nRunning command: ${[cmd, ...argv].join(" ")}\n\n`);

const proc = spawn(cmd, argv, {
...opts,
env: {
Expand All @@ -123,8 +125,6 @@ export const spawnWithLogging = (
},
});

logStream.write(`\nRunning command: ${[cmd, ...argv].join(" ")}\n\n`);

proc.stdout.on("data", (data) => {
const lines: string[] = data.toString().split("\n");

Expand Down
2 changes: 2 additions & 0 deletions packages/create-cloudflare/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const run = async () => {
bundle: true,
outdir: "./dist",
platform: "node",
// This is required to support jsonc-parser. See https://github.com/microsoft/node-jsonc-parser/issues/57
mainFields: ["module", "main"],
format: "cjs",
};

Expand Down
15 changes: 9 additions & 6 deletions packages/create-cloudflare/src/__tests__/workers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe("addWorkersTypesToTsConfig", () => {

// Mock the read of tsconfig.json
vi.mocked(readFile).mockImplementation(
() => `{types: ["@cloudflare/workers-types"]}`
() => `{ "compilerOptions": { "types": ["@cloudflare/workers-types"]} }`
);
});

Expand All @@ -101,8 +101,10 @@ describe("addWorkersTypesToTsConfig", () => {
test("happy path", async () => {
await addWorkersTypesToTsConfig(ctx);

expect(vi.mocked(writeFile).mock.calls[0][1]).toEqual(
`{types: ["@cloudflare/workers-types/2023-07-01"]}`
expect(writeFile).toHaveBeenCalled();

expect(vi.mocked(writeFile).mock.calls[0][1]).toContain(
`"@cloudflare/workers-types/2023-07-01"`
);
});

Expand All @@ -123,12 +125,13 @@ describe("addWorkersTypesToTsConfig", () => {

test("don't clobber existing entrypoints", async () => {
vi.mocked(readFile).mockImplementation(
() => `{types: ["@cloudflare/workers-types/2021-03-20"]}`
() =>
`{ "compilerOptions": { "types" : ["@cloudflare/workers-types/2021-03-20"]} }`
);
await addWorkersTypesToTsConfig(ctx);

expect(vi.mocked(writeFile).mock.calls[0][1]).toEqual(
`{types: ["@cloudflare/workers-types/2021-03-20"]}`
expect(vi.mocked(writeFile).mock.calls[0][1]).toContain(
`"@cloudflare/workers-types/2021-03-20"`
);
});
});
Expand Down
13 changes: 9 additions & 4 deletions packages/create-cloudflare/src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import degit from "degit";
import { C3_DEFAULTS } from "helpers/cli";
import { readJSON, usesTypescript, writeJSON } from "helpers/files";
import { validateTemplateUrl } from "./validators";
import type { C3Args, C3Context } from "types";
import type { C3Args, C3Context, PackageJson } from "types";

export type TemplateConfig = {
/**
Expand Down Expand Up @@ -67,9 +67,14 @@ export type TemplateConfig = {
*/
configure?: (ctx: C3Context) => Promise<void>;

/** A transformer that is run on the project's `package.json` during the creation step */
/**
* A transformer that is run on the project's `package.json` during the creation step.
*
* The object returned from this function will be deep merged with the original.
* */
transformPackageJson?: (
pkgJson: Record<string, string | object>
pkgJson: PackageJson,
ctx: C3Context
) => Promise<Record<string, string | object>>;

/** An array of flags that will be added to the call to the framework cli during tests.*/
Expand Down Expand Up @@ -428,7 +433,7 @@ export const updatePackageScripts = async (ctx: C3Context) => {
let pkgJson = readJSON(pkgJsonPath);

// Run any transformers defined by the template
const transformed = await ctx.template.transformPackageJson(pkgJson);
const transformed = await ctx.template.transformPackageJson(pkgJson, ctx);
pkgJson = deepmerge(pkgJson, transformed);

writeJSON(pkgJsonPath, pkgJson);
Expand Down
8 changes: 8 additions & 0 deletions packages/create-cloudflare/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ export type C3Context = {
type DeploymentInfo = {
url?: string;
};

export type PackageJson = Record<string, string> & {
name: string;
version: string;
scripts?: Record<string, string>;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
61 changes: 38 additions & 23 deletions packages/create-cloudflare/src/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { spinner } from "@cloudflare/cli/interactive";
import { getWorkerdCompatibilityDate, installPackages } from "helpers/command";
import { readFile, usesTypescript, writeFile } from "helpers/files";
import { detectPackageManager } from "helpers/packages";
import * as jsonc from "jsonc-parser";
import MagicString from "magic-string";
import type { C3Context } from "types";

Expand Down Expand Up @@ -105,32 +106,46 @@ export async function addWorkersTypesToTsConfig(ctx: C3Context) {

const typesEntrypoint = `@cloudflare/workers-types/${entrypointVersion}`;

let updated = tsconfig;
try {
const config = jsonc.parse(tsconfig);
const currentTypes = config.compilerOptions?.types ?? [];

if (tsconfig.match(`@cloudflare/workers-types`)) {
// Don't update existing instances if they contain an explicit entrypoint
if (tsconfig.match(`"@cloudflare/workers-types"`)) {
updated = tsconfig.replace("@cloudflare/workers-types", typesEntrypoint);
}
} else {
try {
// Note: this simple implementation doesn't handle tsconfigs containing comments
// (as it is not needed for the existing use cases)

const tsConfigJson = JSON.parse(tsconfig);
tsConfigJson.compilerOptions ??= {};
tsConfigJson.compilerOptions.types = [
...(tsConfigJson.compilerOptions.types ?? []),
typesEntrypoint,
];
updated = JSON.stringify(tsConfigJson, null, 2);
} catch {
warn("Could not parse tsconfig.json file");
updated = tsconfig;
}
const explicitEntrypoint = (currentTypes as string[]).some((t) =>
t.match(/@cloudflare\/workers-types\/\d{4}-\d{2}-\d{2}/)
);

// If a type declaration with an explicit entrypoint exists, leave the types as is
// Otherwise, add the latest entrypoint
const newTypes = explicitEntrypoint
? [...currentTypes]
: [
...currentTypes.filter(
(t: string) => t !== "@cloudflare/workers-types"
),
typesEntrypoint,
];

// If we detect any tabs, use tabs, otherwise use spaces.
// We need to pass an explicit value here in order to preserve formatting properly.
const useSpaces = !tsconfig.match(/\t/g);

// Calculate required edits and apply them to file
const edits = jsonc.modify(
tsconfig,
["compilerOptions", "types"],
newTypes,
{
formattingOptions: { insertSpaces: useSpaces },
}
);
const updated = jsonc.applyEdits(tsconfig, edits);
writeFile(tsconfigPath, updated);
} catch (error) {
warn(
"Failed to update `tsconfig.json` with latest `@cloudflare/workers-types` entrypoint."
);
}

writeFile(tsconfigPath, updated);
s.stop(`${brandColor("added")} ${dim(typesEntrypoint)}`);
}

Expand Down
108 changes: 80 additions & 28 deletions packages/create-cloudflare/templates/svelte/c3.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { platform } from "node:os";
import { logRaw, updateStatus } from "@cloudflare/cli";
import { blue, brandColor, dim } from "@cloudflare/cli/colors";
import { parseTs, transformFile } from "helpers/codemod";
import { transformFile } from "helpers/codemod";
import { installPackages, runFrameworkGenerator } from "helpers/command";
import { compatDateFlag, usesTypescript } from "helpers/files";
import { usesTypescript } from "helpers/files";
import { detectPackageManager } from "helpers/packages";
import { platformInterface } from "./templates";
import * as recast from "recast";
import type { TemplateConfig } from "../../src/templates";
import type * as recast from "recast";
import type { C3Context } from "types";
import type { C3Context, PackageJson } from "types";

const { npm } = detectPackageManager();

Expand All @@ -26,7 +26,14 @@ const configure = async (ctx: C3Context) => {
doneText: `${brandColor(`installed`)} ${dim(pkg)}`,
});

// Change the import statement in svelte.config.js
updateSvelteConfig();
updateTypeDefinitions(ctx);
};

const updateSvelteConfig = () => {
// All we need to do is change the import statement in svelte.config.js
updateStatus(`Changing adapter in ${blue("svelte.config.js")}`);

transformFile("svelte.config.js", {
visitImportDeclaration: function (n) {
// importSource is the `x` in `import y from "x"`
Expand All @@ -39,39 +46,84 @@ const configure = async (ctx: C3Context) => {
return false;
},
});
updateStatus(`Changing adapter in ${blue("svelte.config.js")}`);
};

// If using typescript, add the platform interface to the `App` interface
if (usesTypescript(ctx)) {
transformFile("src/app.d.ts", {
visitTSModuleDeclaration(n) {
if (n.value.id.name === "App") {
const patchAst = parseTs(platformInterface);
const body = n.node.body as recast.types.namedTypes.TSModuleBlock;
body.body.push(patchAst.program.body[0]);
}

this.traverse(n);
},
});
updateStatus(`Updating global type definitions in ${blue("app.d.ts")}`);
const updateTypeDefinitions = (ctx: C3Context) => {
if (!usesTypescript(ctx)) {
return;
}

updateStatus(`Updating global type definitions in ${blue("app.d.ts")}`);

const b = recast.types.builders;

transformFile("src/app.d.ts", {
visitTSModuleDeclaration(n) {
if (n.value.id.name === "App" && n.node.body) {
const moduleBlock = n.node
.body as recast.types.namedTypes.TSModuleBlock;

const platformInterface = b.tsInterfaceDeclaration(
b.identifier("Platform"),
b.tsInterfaceBody([
b.tsPropertySignature(
b.identifier("env"),
b.tsTypeAnnotation(b.tsTypeReference(b.identifier("Env")))
),
b.tsPropertySignature(
b.identifier("cf"),
b.tsTypeAnnotation(
b.tsTypeReference(b.identifier("CfProperties"))
)
),
b.tsPropertySignature(
b.identifier("ctx"),
b.tsTypeAnnotation(
b.tsTypeReference(b.identifier("ExecutionContext"))
)
),
])
);

moduleBlock.body.unshift(platformInterface);
}

this.traverse(n);
},
});
};

const config: TemplateConfig = {
configVersion: 1,
id: "svelte",
displayName: "Svelte",
platform: "pages",
copyFiles: {
variants: {
js: { path: "./js" },
ts: { path: "./ts" },
},
},
generate,
configure,
transformPackageJson: async () => ({
scripts: {
"pages:preview": `${npm} run build && wrangler pages dev ${await compatDateFlag()} .svelte-kit/cloudflare`,
"pages:deploy": `${npm} run build && wrangler pages deploy .svelte-kit/cloudflare`,
},
}),
transformPackageJson: async (original: PackageJson, ctx: C3Context) => {
let scripts: Record<string, string> = {
preview: `${npm} run build && wrangler pages dev .svelte-kit/cloudflare`,
deploy: `${npm} run build && wrangler pages deploy .svelte-kit/cloudflare`,
};

if (usesTypescript(ctx)) {
const mv = platform() === "win32" ? "move" : "mv";
scripts = {
...scripts,
"build-cf-types": `wrangler types && ${mv} worker-configuration.d.ts src/`,
};
}

return { scripts };
},
devScript: "dev",
previewScript: "pages:preview",
deployScript: "deploy",
previewScript: "preview",
};
export default config;
Loading

0 comments on commit a751489

Please sign in to comment.