Skip to content

Commit df1f9c9

Browse files
Support Waku in autoconfig (#11692)
1 parent 7f12c81 commit df1f9c9

File tree

9 files changed

+305
-1
lines changed

9 files changed

+305
-1
lines changed

.changeset/chilly-camels-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-cloudflare": minor
3+
---
4+
5+
Support Waku in `--experimental` mode

.changeset/tall-jobs-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Support Waku in autoconfig

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"userconfig",
5656
"versionless",
5757
"vike",
58+
"Waku",
5859
"wasmvalue",
5960
"weakmap",
6061
"weakset",

packages/create-cloudflare/e2e/tests/cli/cli.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ describe("Create Cloudflare CLI", () => {
555555
npm create cloudflare -- --framework svelte -- --types=ts
556556
pnpm create cloudflare --framework svelte -- --types=ts
557557
Allowed Values:
558-
analog, angular, astro, docusaurus, gatsby, next, nuxt, qwik, react, react-router, redwood, solid, svelte, tanstack-start, vike, vue
558+
analog, angular, astro, docusaurus, gatsby, next, nuxt, qwik, react, react-router, redwood, solid, svelte, tanstack-start, vike, vue, waku
559559
--platform=<value>
560560
Whether the application should be deployed to Pages or Workers. This is only applicable for Frameworks templates that support both Pages and Workers.
561561
Allowed Values:

packages/create-cloudflare/e2e/tests/frameworks/test-config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,22 @@ function getExperimentalFrameworkTestConfig(
10141014
],
10151015
verifyTypes: false,
10161016
},
1017+
{
1018+
name: "waku",
1019+
testCommitMessage: true,
1020+
timeout: LONG_TIMEOUT,
1021+
unsupportedOSs: ["win32"],
1022+
verifyDeploy: {
1023+
route: "/",
1024+
expectedText: "Waku",
1025+
},
1026+
verifyPreview: {
1027+
route: "/",
1028+
expectedText: "Waku",
1029+
},
1030+
nodeCompat: false,
1031+
verifyTypes: false,
1032+
},
10171033
];
10181034
}
10191035

packages/create-cloudflare/src/templates.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import vikeTemplate from "templates/vike/c3";
4949
import vikeExperimentalTemplate from "templates/vike/experimental-c3";
5050
import vueTemplate from "templates/vue/c3";
5151
import wakuTemplate from "templates/waku/c3";
52+
import wakuExperimentalTemplate from "templates/waku/experimental-c3";
5253
import { isInsideGitRepo } from "./git";
5354
import { validateProjectDirectory, validateTemplateUrl } from "./validators";
5455
import type { Option } from "@cloudflare/cli/interactive";
@@ -255,6 +256,7 @@ export function getFrameworkMap({ experimental = false }): TemplateMap {
255256
"tanstack-start": tanStackStartTemplate,
256257
vike: vikeExperimentalTemplate,
257258
vue: vueTemplate,
259+
waku: wakuExperimentalTemplate,
258260
};
259261
} else {
260262
return {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { runFrameworkGenerator } from "frameworks/index";
2+
import { detectPackageManager } from "helpers/packageManagers";
3+
import type { TemplateConfig } from "../../src/templates";
4+
import type { C3Context } from "types";
5+
6+
const { npm } = detectPackageManager();
7+
8+
const generate = async (ctx: C3Context) => {
9+
await runFrameworkGenerator(ctx, [
10+
"--project-name",
11+
ctx.project.name,
12+
// Note: we could point the waku create CLI to a cloudflare-ready template, that works great
13+
// but here we don't want to use that because we do want to exercise the general
14+
// autoconfig/`wrangler setup` functionality (which amongst other things lets up ensure
15+
// that we can support the migration of existing waku projects)
16+
]);
17+
};
18+
19+
const config: TemplateConfig = {
20+
configVersion: 1,
21+
id: "waku",
22+
frameworkCli: "create-waku",
23+
platform: "workers",
24+
displayName: "Waku",
25+
generate,
26+
devScript: "dev",
27+
deployScript: "deploy",
28+
previewScript: "preview",
29+
};
30+
export default config;

packages/wrangler/src/autoconfig/frameworks/get-framework.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SvelteKit } from "./sveltekit";
1212
import { TanstackStart } from "./tanstack";
1313
import { Vike } from "./vike";
1414
import { Vite } from "./vite";
15+
import { Waku } from "./waku";
1516
import type { Framework } from ".";
1617

1718
export function getFramework(detectedFramework?: {
@@ -45,6 +46,8 @@ export function getFramework(detectedFramework?: {
4546
return new Hono(detectedFramework.name);
4647
case "vike":
4748
return new Vike(detectedFramework.name);
49+
case "waku":
50+
return new Waku(detectedFramework.name);
4851
default:
4952
return new Static(detectedFramework?.name);
5053
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import assert from "node:assert";
2+
import { existsSync } from "node:fs";
3+
import { mkdir, writeFile } from "node:fs/promises";
4+
import { join } from "node:path";
5+
import { updateStatus } from "@cloudflare/cli";
6+
import { blue, brandColor } from "@cloudflare/cli/colors";
7+
import * as recast from "recast";
8+
import dedent from "ts-dedent";
9+
import { transformFile } from "../c3-vendor/codemod";
10+
import { installPackages } from "../c3-vendor/packages";
11+
import { Framework } from ".";
12+
import type { ConfigurationOptions, ConfigurationResults } from ".";
13+
import type { types } from "recast";
14+
15+
const b = recast.types.builders;
16+
const t = recast.types.namedTypes;
17+
18+
export class Waku extends Framework {
19+
async configure({
20+
dryRun,
21+
projectPath,
22+
}: ConfigurationOptions): Promise<ConfigurationResults> {
23+
if (!dryRun) {
24+
await installPackages(["hono", "@hiogawa/node-loader-cloudflare"], {
25+
dev: true,
26+
startText: "Installing additional dependencies",
27+
doneText: `${brandColor("installed")}`,
28+
});
29+
30+
await createCloudflareMiddleware(projectPath);
31+
await createServerEntryFile(projectPath);
32+
await updateWakuConfig(projectPath);
33+
}
34+
35+
return {
36+
wranglerConfig: {
37+
main: "./dist/server/serve-cloudflare.js",
38+
compatibility_flags: ["nodejs_compat"],
39+
assets: {
40+
binding: "ASSETS",
41+
directory: "./dist/public",
42+
html_handling: "drop-trailing-slash",
43+
not_found_handling: "404-page",
44+
},
45+
},
46+
};
47+
}
48+
}
49+
50+
/**
51+
* Created a server-entry file that uses the Cloudflare middleware
52+
*
53+
* @param projectPath Path to the project
54+
*/
55+
async function createServerEntryFile(projectPath: string) {
56+
await writeFile(
57+
`${projectPath}/src/server-entry.tsx`,
58+
dedent`
59+
/// <reference types="vite/client" />
60+
import { contextStorage } from 'hono/context-storage';
61+
import { fsRouter } from 'waku';
62+
import adapter from 'waku/adapters/cloudflare';
63+
import cloudflareMiddleware from './middleware/cloudflare';
64+
65+
export default adapter(
66+
fsRouter(import.meta.glob('./**/*.tsx', { base: './pages' })),
67+
{ middlewareFns: [contextStorage, cloudflareMiddleware] },
68+
);
69+
`
70+
);
71+
}
72+
73+
/**
74+
* Created the middleware/cloudflare.ts file
75+
*
76+
* @param projectPath The path for the project
77+
*/
78+
async function createCloudflareMiddleware(projectPath: string) {
79+
const middlewareDir = `${projectPath}/src/middleware`;
80+
81+
await mkdir(middlewareDir, { recursive: true });
82+
83+
await writeFile(
84+
`${middlewareDir}/cloudflare.ts`,
85+
dedent`
86+
import type { Context, MiddlewareHandler } from 'hono';
87+
88+
function isWranglerDev(c: Context): boolean {
89+
// This header seems to only be set for production cloudflare workers
90+
return !c.req.header('cf-visitor');
91+
}
92+
93+
const cloudflareMiddleware = (): MiddlewareHandler => {
94+
return async (c, next) => {
95+
await next();
96+
if (!import.meta.env?.PROD) {
97+
return;
98+
}
99+
if (!isWranglerDev(c)) {
100+
return;
101+
}
102+
const contentType = c.res.headers.get('content-type');
103+
if (
104+
!contentType ||
105+
contentType.includes('text/html') ||
106+
contentType.includes('text/plain')
107+
) {
108+
const headers = new Headers(c.res.headers);
109+
headers.set('content-encoding', 'Identity');
110+
c.res = new Response(c.res.body, {
111+
status: c.res.status,
112+
statusText: c.res.statusText,
113+
headers: c.res.headers,
114+
});
115+
}
116+
};
117+
};
118+
119+
export default cloudflareMiddleware;
120+
`
121+
);
122+
}
123+
124+
/**
125+
* Updated the waku.config.ts file to import and use the @hiogawa/node-loader-cloudflare plugin
126+
*
127+
* @param projectPath Path to the project
128+
*/
129+
async function updateWakuConfig(projectPath: string) {
130+
const wakuConfigPath = join(projectPath, "waku.config.ts");
131+
132+
if (!existsSync(wakuConfigPath)) {
133+
throw new Error("Could not find Waku config file to modify");
134+
}
135+
136+
updateStatus(`Updating Waku configuration in ${blue(wakuConfigPath)}`);
137+
138+
transformFile(wakuConfigPath, {
139+
visitProgram(n) {
140+
// Add an import of the @hiogawa/node-loader-cloudflare/vite
141+
// ```
142+
// import nodeLoaderCloudflare from '@hiogawa/node-loader-cloudflare/vite;
143+
// ```
144+
const lastImportIndex = n.node.body.findLastIndex(
145+
(statement) => statement.type === "ImportDeclaration"
146+
);
147+
const lastImport = n.get("body", lastImportIndex);
148+
149+
// Only import if not already imported
150+
if (
151+
!n.node.body.some(
152+
(s) =>
153+
s.type === "ImportDeclaration" &&
154+
s.source.value === "@hiogawa/node-loader-cloudflare/vite"
155+
)
156+
) {
157+
const importAst = b.importDeclaration(
158+
[b.importDefaultSpecifier(b.identifier("nodeLoaderCloudflare"))],
159+
b.stringLiteral("@hiogawa/node-loader-cloudflare/vite")
160+
);
161+
lastImport.insertAfter(importAst);
162+
}
163+
164+
return this.traverse(n);
165+
},
166+
visitCallExpression: function (n) {
167+
const callee = n.node.callee as types.namedTypes.Identifier;
168+
if (callee.name !== "defineConfig") {
169+
return this.traverse(n);
170+
}
171+
172+
const config = n.node.arguments[0];
173+
assert(t.ObjectExpression.check(config));
174+
const viteConfig = config.properties.find((prop) =>
175+
isViteProp(prop)
176+
)?.value;
177+
assert(t.ObjectExpression.check(viteConfig));
178+
const pluginsProp = viteConfig.properties.find((prop) =>
179+
isPluginsProp(prop)
180+
);
181+
assert(pluginsProp && t.ArrayExpression.check(pluginsProp.value));
182+
183+
// Only add the Cloudflare loader plugin if it's not already present
184+
if (
185+
!pluginsProp.value.elements.some(
186+
(el) =>
187+
el?.type === "CallExpression" &&
188+
el.callee.type === "Identifier" &&
189+
el.callee.name === "nodeLoaderCloudflare"
190+
)
191+
) {
192+
pluginsProp.value.elements.push(
193+
b.callExpression(b.identifier("nodeLoaderCloudflare"), [
194+
b.objectExpression([
195+
b.objectProperty(
196+
b.identifier("environments"),
197+
b.arrayExpression([b.stringLiteral("rsc")])
198+
),
199+
b.objectProperty(b.identifier("build"), b.booleanLiteral(true)),
200+
b.objectProperty(
201+
b.identifier("getPlatformProxyOptions"),
202+
b.objectExpression([
203+
b.objectProperty(
204+
b.identifier("persist"),
205+
b.objectExpression([
206+
b.objectProperty(
207+
b.identifier("path"),
208+
b.stringLiteral(".wrangler/state/v3")
209+
),
210+
])
211+
),
212+
])
213+
),
214+
]),
215+
])
216+
);
217+
}
218+
219+
this.traverse(n);
220+
},
221+
});
222+
}
223+
224+
function isViteProp(
225+
prop: unknown
226+
): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property {
227+
return (
228+
(t.Property.check(prop) || t.ObjectProperty.check(prop)) &&
229+
t.Identifier.check(prop.key) &&
230+
prop.key.name === "vite"
231+
);
232+
}
233+
234+
function isPluginsProp(
235+
prop: unknown
236+
): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property {
237+
return (
238+
(t.Property.check(prop) || t.ObjectProperty.check(prop)) &&
239+
t.Identifier.check(prop.key) &&
240+
prop.key.name === "plugins"
241+
);
242+
}

0 commit comments

Comments
 (0)