Skip to content
Open
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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ Cloudflare's Templates Platform extracts `name`, `description`, and a `cloudflar
- Can only be provided by a Cloudflare team member. Image files for icons and preview images are stored in the Cloudflare Templates CF account.
- Preview icon should be an SVG representing the application.

### Worker source maps

Enable `upload_source_maps` in every template's Wrangler configuration. When a framework builds the Worker, also enable external source maps in its server build. With the Cloudflare Vite plugin, set `build.sourcemap: true` for the Worker environment (`worker` or `ssr`), rather than the client environment.

Run `pnpm check:source-maps` after building the templates to verify that each Worker emits usable source maps and that server maps are excluded from public assets. Browser source maps remain a separate choice.

### `package-lock.json`

All of our templates and Deploy to Cloudflare projects automatically set up Workers CI. In our testing, including a package lock file in the template repository speeds up module resolution by 80% or more.
Expand Down
5 changes: 4 additions & 1 deletion agent-commerce-analytics-template/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
plugins: [react(), cloudflare()],
environments: {
worker: { build: { sourcemap: true } },
},
plugins: [react(), cloudflare({ viteEnvironment: { name: "worker" } })],
});
8 changes: 7 additions & 1 deletion agent-visibility-template/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
environments: {
worker: { build: { sourcemap: true } },
},
optimizeDeps: {
exclude: ["hono", "hono/cors"],
},
plugins: [react(), cloudflare({ remoteBindings: false })],
plugins: [
react(),
cloudflare({ viteEnvironment: { name: "worker" }, remoteBindings: false }),
],
});
3 changes: 3 additions & 0 deletions ai-brand-visibility-template/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
environments: {
ssr: { build: { sourcemap: true } },
},
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" }, remoteBindings: false }),
tailwindcss(),
Expand Down
16 changes: 15 additions & 1 deletion astro-blog-starter-template/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@ import cloudflare from "@astrojs/cloudflare";
// https://astro.build/config
export default defineConfig({
site: "https://example.com",
integrations: [mdx(), sitemap()],
integrations: [
{
name: "worker-source-maps",
hooks: {
"astro:build:setup": ({ target, vite }) => {
if (target === "server") {
vite.build ??= {};
vite.build.sourcemap = true;
}
},
},
},
mdx(),
sitemap(),
],
adapter: cloudflare({
platformProxy: {
enabled: true,
Expand Down
15 changes: 15 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { depsInfo } from "./depsInfo";
import { depsUpdate } from "./depsUpdate";
import deployLiveDemos from "./deployLiveDemos";
import { preview } from "./preview";
import { checkSourceMaps } from "./sourceMaps";

const program = new Command();

Expand Down Expand Up @@ -64,6 +65,20 @@ program
);
});

program
.command("check-source-maps")
.description("validate external Worker maps after framework builds")
.argument(
"[path-to-template(s)]",
"path to directory containing template(s)",
".",
)
.action((templateDirectory) =>
actionWithSummary("Worker source maps", () =>
checkSourceMaps(templateDirectory),
),
);

program
.command("lint")
.description("find and fix template style problems")
Expand Down
123 changes: 123 additions & 0 deletions cli/src/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import fs from "node:fs";
import path from "node:path";
import MarkdownError from "./MarkdownError";
import { getAllTemplates, readJsonC } from "./util";

type WorkerBuildConfig = {
main?: string;
upload_source_maps?: boolean;
assets?: { directory?: string };
};

/** Check framework output after building. Wrangler generates maps for unbundled starters. */
export function checkSourceMaps(templateDirectory: string): void {
const problems: string[] = [];
for (const template of getAllTemplates(templateDirectory)) {
const isFramework = fs
.readdirSync(template.path)
.some((file) => /^(vite|astro)\.config\.[cm]?[jt]s$/.test(file));
if (!isFramework) continue;
try {
checkFrameworkSourceMaps(template.path);
} catch (error) {
problems.push(`${template.name}: ${(error as Error).message}`);
}
}
if (problems.length) {
throw new MarkdownError(
"Worker source map checks failed",
problems.map((problem) => `- ${problem}`).join("\n"),
);
}
}

export function checkFrameworkSourceMaps(templatePath: string): void {
const redirectPath = path.join(templatePath, ".wrangler/deploy/config.json");
const rootConfigPath = ["wrangler.jsonc", "wrangler.json"]
.map((file) => path.join(templatePath, file))
.find(fs.existsSync);
if (!rootConfigPath) throw new Error("Missing Wrangler configuration");
const configPath = fs.existsSync(redirectPath)
? path.resolve(
path.dirname(redirectPath),
(readJsonC(redirectPath) as { configPath: string }).configPath,
)
: rootConfigPath;
if (!fs.existsSync(configPath))
throw new Error(
"Missing generated Wrangler configuration; build the template first",
);
const config = readJsonC(configPath) as WorkerBuildConfig;
if (config.upload_source_maps !== true)
throw new Error("Built Worker must enable upload_source_maps");
const astroWorkerDir = path.join(templatePath, "dist/_worker.js");
const workerDir = fs.existsSync(astroWorkerDir)
? astroWorkerDir
: configPath !== rootConfigPath
? path.dirname(configPath)
: path.join(templatePath, "build/server");
if (!fs.existsSync(workerDir))
throw new Error("Missing Worker build output; build the template first");
const assetsDir =
config.assets?.directory &&
path.resolve(path.dirname(configPath), config.assets.directory);
let mapCount = 0;
for (const file of fs.readdirSync(workerDir, { recursive: true })) {
if (!/\.[cm]?js$/.test(file.toString())) continue;
const jsPath = path.join(workerDir, file.toString());
const code = fs.readFileSync(jsPath, "utf8");
const reference = /\/\/# sourceMappingURL=(\S+)/.exec(code)?.[1];
if (!reference) continue;
if (reference.startsWith("data:"))
throw new Error(`${file} uses inline maps; emit external maps`);
const mapPath = path.resolve(path.dirname(jsPath), reference);
if (!fs.existsSync(mapPath))
throw new Error(`${file} references a missing source map`);
const map = JSON.parse(fs.readFileSync(mapPath, "utf8"));
if (
map.version !== 3 ||
!Array.isArray(map.sources) ||
typeof map.mappings !== "string"
)
throw new Error(`${file} has an invalid source map`);
if (assetsDir && isInside(assetsDir, mapPath)) {
const ignorePath = path.join(assetsDir, ".assetsignore");
const ignored =
fs.existsSync(ignorePath) &&
fs
.readFileSync(ignorePath, "utf8")
.split(/\r?\n/)
.some((pattern) => {
// Worker directories are excluded as a whole, including their maps.
const directory = pattern
.trim()
.replace(/^\//, "")
.replace(/\/\*\*$/, "");
return (
directory &&
!directory.includes("*") &&
isInside(path.join(assetsDir, directory), mapPath)
);
});
if (!ignored)
throw new Error(
`${file}'s server map is inside public assets; exclude its Worker directory in .assetsignore`,
);
}
// Generated re-export entry files can have empty maps; require mapped source in the chunks.
if (map.sources.length && map.mappings.length) mapCount++;
}
if (!mapCount)
throw new Error(
"No external Worker source maps found; enable maps in the server build",
);
}

function isInside(directory: string, file: string): boolean {
const relative = path.relative(directory, file);
return (
relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative)
);
}
85 changes: 85 additions & 0 deletions cli/test/sourceMaps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, expect, test } from "vitest";
import { checkFrameworkSourceMaps } from "../src/sourceMaps";

let root: string;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), "template-source-maps-"));
fs.mkdirSync(path.join(root, ".wrangler/deploy"), { recursive: true });
fs.mkdirSync(path.join(root, "dist/worker"), { recursive: true });
fs.writeFileSync(
path.join(root, "wrangler.jsonc"),
'{"main":"src/index.ts","upload_source_maps":true}',
);
fs.writeFileSync(
path.join(root, ".wrangler/deploy/config.json"),
'{"configPath":"../../dist/worker/wrangler.json"}',
);
fs.writeFileSync(
path.join(root, "dist/worker/wrangler.json"),
'{"main":"index.js","upload_source_maps":true,"assets":{"directory":"../client"}}',
);
fs.writeFileSync(
path.join(root, "dist/worker/index.js"),
'throw new Error("original source");\n//# sourceMappingURL=index.js.map',
);
fs.writeFileSync(
path.join(root, "dist/worker/index.js.map"),
JSON.stringify({
version: 3,
sources: ["../../src/index.ts"],
mappings: "AAAA",
}),
);
});
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));

test("validates the built config and external Worker maps", () => {
expect(() => checkFrameworkSourceMaps(root)).not.toThrow();
});
test("rejects uploads disabled in the generated config", () => {
fs.writeFileSync(
path.join(root, "dist/worker/wrangler.json"),
'{"upload_source_maps":false}',
);
expect(() => checkFrameworkSourceMaps(root)).toThrow(
"Built Worker must enable upload_source_maps",
);
});
test("rejects missing maps despite the upload flag", () => {
fs.rmSync(path.join(root, "dist/worker/index.js.map"));
expect(() => checkFrameworkSourceMaps(root)).toThrow(
"references a missing source map",
);
});
test("rejects builds with no map references", () => {
fs.writeFileSync(
path.join(root, "dist/worker/index.js"),
"export default {};",
);
expect(() => checkFrameworkSourceMaps(root)).toThrow(
"No external Worker source maps found",
);
});
test("rejects builds containing only empty maps", () => {
fs.writeFileSync(
path.join(root, "dist/worker/index.js.map"),
'{"version":3,"sources":[],"mappings":""}',
);
expect(() => checkFrameworkSourceMaps(root)).toThrow(
"No external Worker source maps found",
);
});
test("requires server maps to be excluded from public assets", () => {
fs.writeFileSync(
path.join(root, "dist/worker/wrangler.json"),
'{"upload_source_maps":true,"assets":{"directory":".."}}',
);
expect(() => checkFrameworkSourceMaps(root)).toThrow(
"server map is inside public assets",
);
fs.writeFileSync(path.join(root, "dist/.assetsignore"), "worker\n");
expect(() => checkFrameworkSourceMaps(root)).not.toThrow();
});
8 changes: 7 additions & 1 deletion commerce-llms-txt-template/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
plugins: [react(), cloudflare({ remoteBindings: false })],
environments: {
worker: { build: { sourcemap: true } },
},
plugins: [
react(),
cloudflare({ viteEnvironment: { name: "worker" }, remoteBindings: false }),
],
});
4 changes: 4 additions & 0 deletions next-starter-template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Worker source maps

This template enables `upload_source_maps` in Wrangler. OpenNext currently discards source maps when bundling its server, so production stacks can identify bundled server code but cannot reliably map back to the original Next.js source. Follow [OpenNext source map support](https://github.com/opennextjs/opennextjs-cloudflare/issues/1124) for adapter support. The repository's framework source map check covers Vite and Astro builds; OpenNext remains excluded until it preserves maps through its bundling and patching steps.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
]
},
"scripts": {
"check": "pnpm run check:deps && pnpm run check:templates && pnpm run check:lockfiles && pnpm run check:turbo && pnpm run check:prettier && git diff --exit-code",
"check": "pnpm run check:deps && pnpm run check:templates && pnpm run check:lockfiles && pnpm run check:turbo && pnpm run check:source-maps && pnpm run check:prettier && git diff --exit-code",
"check:deps": "syncpack lint",
"check:lockfiles": "templates lint-npm-lockfiles .",
"check:prettier": "prettier . --check",
Expand All @@ -51,6 +51,7 @@
"upload": "templates upload",
"validate-d2c-buttons": "templates validate-d2c-buttons",
"validate-live-demo-links": "templates validate-live-demo-links",
"validate-version-private-package-json": "templates validate-version-private-package-json"
"validate-version-private-package-json": "templates validate-version-private-package-json",
"check:source-maps": "templates check-source-maps ."
}
}
5 changes: 4 additions & 1 deletion react-postgres-fullstack-template/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
plugins: [react(), cloudflare()],
environments: {
worker: { build: { sourcemap: true } },
},
plugins: [react(), cloudflare({ viteEnvironment: { name: "worker" } })],
});
3 changes: 3 additions & 0 deletions react-router-hono-fullstack-template/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
environments: {
ssr: { build: { sourcemap: true } },
},
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
tailwindcss(),
Expand Down
3 changes: 3 additions & 0 deletions react-router-postgres-ssr-template/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
environments: {
ssr: { build: { sourcemap: true } },
},
plugins: [cloudflare({ viteEnvironment: { name: "ssr" } }), reactRouter()],
});
3 changes: 3 additions & 0 deletions react-router-starter-template/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
environments: {
ssr: { build: { sourcemap: true } },
},
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
tailwindcss(),
Expand Down
5 changes: 4 additions & 1 deletion react-starter-template/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
plugins: [react(), cloudflare()],
environments: {
worker: { build: { sourcemap: true } },
},
plugins: [react(), cloudflare({ viteEnvironment: { name: "worker" } })],
});
Loading
Loading