Skip to content

Commit

Permalink
feat: allow for Pages projects to upload sourcemaps
Browse files Browse the repository at this point in the history
  • Loading branch information
zebp committed May 17, 2024
1 parent 65902d5 commit 0de4833
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 124 deletions.
7 changes: 7 additions & 0 deletions .changeset/old-horses-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

feat: allow for Pages projects to upload sourcemaps

Pages projects can now upload sourcemaps for server bundles to enable remapped stacktraces in realtime logs when deployed with `upload_source_map` set to `true` in `wrangler.toml`.
287 changes: 203 additions & 84 deletions packages/wrangler/src/__tests__/pages/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4741,107 +4741,117 @@ and that at least one include rule is provided.
});
});

describe("_worker.js bundling", () => {
beforeEach(() => {
mkdirSync("public");
writeFileSync(
"public/_worker.js",
`
export default {
async fetch(request, env) {
return new Response('Ok');
}
};
`
);
});
const simulateServer = (
generatedWorkerBundleCheck: (workerJsContent: string) => void,
compatibility_flags?: string[]
) => {
mockGetUploadTokenRequest(
"<<funfetti-auth-jwt>>",
"some-account-id",
"foo"
);

const workerIsBundled = (contents: string) =>
contents.includes("worker_default as default");
msw.use(
rest.post("*/pages/assets/check-missing", async (req, res, ctx) =>
res.once(
ctx.status(200),
ctx.json({
success: true,
errors: [],
messages: [],
result: (await req.json()).hashes,
})
)
),
rest.post("*/pages/assets/upload", async (_req, res, ctx) =>
res.once(
ctx.status(200),
ctx.json({
success: true,
errors: [],
messages: [],
result: null,
})
)
),
rest.post(
"*/accounts/:accountId/pages/projects/foo/deployments",
async (req, res, ctx) => {
const body = await (req as RestRequestWithFormData).formData();
const generatedWorkerBundle = body.get("_worker.bundle") as string;
const workerBundleWithConstantData = generatedWorkerBundle
.replace(
/------formdata-undici-0.[0-9]*/g,
"------formdata-undici-0.test"
)
.replace(/bundledWorker-0.[0-9]*.mjs/g, "bundledWorker-0.test.mjs")
.replace(
/functionsWorker-0.[0-9]*.js/g,
"functionsWorker-0.test.js"
);

const simulateServer = (
generatedWorkerBundleCheck: (workerJsContent: string) => void,
compatibility_flags?: string[]
) => {
mockGetUploadTokenRequest(
"<<funfetti-auth-jwt>>",
"some-account-id",
"foo"
);
generatedWorkerBundleCheck(workerBundleWithConstantData);

msw.use(
rest.post("*/pages/assets/check-missing", async (req, res, ctx) =>
res.once(
return res.once(
ctx.status(200),
ctx.json({
success: true,
errors: [],
messages: [],
result: (await req.json()).hashes,
result: {
id: "123-456-789",
url: "https://abcxyz.foo.pages.dev/",
},
})
)
),
rest.post("*/pages/assets/upload", async (_req, res, ctx) =>
res.once(
);
}
),
rest.get(
"*/accounts/:accountId/pages/projects/foo/deployments/:deploymentId",
async (req, res, ctx) => {
expect(req.params.accountId).toEqual("some-account-id");
expect(req.params.deploymentId).toEqual("123-456-789");

return res.once(
ctx.status(200),
ctx.json({
success: true,
errors: [],
messages: [],
result: null,
})
)
),
rest.post(
"*/accounts/:accountId/pages/projects/foo/deployments",
async (req, res, ctx) => {
const body = await (req as RestRequestWithFormData).formData();
const generatedWorkerBundle = body.get("_worker.bundle") as string;

generatedWorkerBundleCheck(generatedWorkerBundle);

return res.once(
ctx.status(200),
ctx.json({
success: true,
errors: [],
messages: [],
result: {
id: "123-456-789",
url: "https://abcxyz.foo.pages.dev/",
result: {
latest_stage: {
name: "deploy",
status: "success",
},
})
);
}
),
rest.get(
"*/accounts/:accountId/pages/projects/foo/deployments/:deploymentId",
async (req, res, ctx) => {
expect(req.params.accountId).toEqual("some-account-id");
expect(req.params.deploymentId).toEqual("123-456-789");
},
})
);
}
),
// we're expecting two API calls to `/projects/<name>`, so we need
// to mock both of them
mockGetProjectHandler("foo", compatibility_flags),
mockGetProjectHandler("foo", compatibility_flags)
);
};

return res.once(
ctx.status(200),
ctx.json({
success: true,
errors: [],
messages: [],
result: {
latest_stage: {
name: "deploy",
status: "success",
},
},
})
);
}
),
// we're expecting two API calls to `/projects/<name>`, so we need
// to mock both of them
mockGetProjectHandler("foo", compatibility_flags),
mockGetProjectHandler("foo", compatibility_flags)
describe("_worker.js bundling", () => {
beforeEach(() => {
mkdirSync("public");
writeFileSync(
"public/_worker.js",
`
export default {
async fetch(request, env) {
return new Response('Ok');
}
};
`
);
};
});

const workerIsBundled = (contents: string) =>
contents.includes("worker_default as default");

it("should bundle the _worker.js when both `--bundle` and `--no-bundle` are omitted", async () => {
simulateServer((generatedWorkerJS) =>
Expand Down Expand Up @@ -4989,6 +4999,115 @@ and that at least one include rule is provided.
expect(std.out).toContain("✨ Uploading Worker bundle");
});
});

describe("source maps", () => {
beforeEach(() => {
mkdirSync("dist");
writeFileSync(
"wrangler.toml",
`
name = "foo"
pages_build_output_dir = "dist"
compatibility_date = "2024-01-01"
upload_source_maps = true
`
);
});

it("should upload sourcemaps for functions directory projects", async () => {
mkdirSync("functions");
writeFileSync(
"functions/[[path]].ts",
`
export function onRequestGet() {
return new Response("")
};
`
);

simulateServer((contents) => {
// Ensure we get a sourcemap containing our functions file
expect(contents).toContain(
'Content-Disposition: form-data; name="functionsWorker-0.test.js.map"'
);
expect(contents).toContain('"sources":["[[path]].ts"');
});

await runWrangler("pages deploy");
});

it("should upload sourcemaps for _worker.js file projects", async () => {
writeFileSync(
"dist/_worker.js",
`
export default {
async fetch() {
return new Response("foo");
}
}
`
);

simulateServer((contents) => {
// Ensure we get a sourcemap containing our _worker.js file
expect(contents).toContain(
'Content-Disposition: form-data; name="bundledWorker-0.test.mjs.map"'
);
expect(contents).toContain('"sources":["_worker.js"');
});

await runWrangler("pages deploy");
});

it("should upload sourcemaps for _worker.js directory projects", async () => {
mkdirSync("dist/_worker.js");
mkdirSync("dist/_worker.js/chunks");
writeFileSync(
"dist/_worker.js/index.js",
`
export { handlers as default } from "./chunks/runtime.mjs";
`
);

writeFileSync(
"dist/_worker.js/chunks/runtime.mjs",
`
export const handlers = {};
// We need this all the way at the start of the line because of how we detect sourcemap urls
//# sourceMappingURL=runtime.mjs.map`
);
writeFileSync(
"dist/_worker.js/chunks/runtime.mjs.map",
JSON.stringify({
version: 3,
file: "runtime.mjs",
sources: [],
sourcesContent: null,
names: [],
mappings: "",
})
);

simulateServer((contents) => {
// Ensure we get a sourcemap containing our main worker file
expect(contents).toContain(
'Content-Disposition: form-data; name="bundledWorker-0.test.mjs.map"'
);
expect(contents).toContain('"sources":["dist/_worker.js/index.js"');

// Ensure our runtime file that wrangler doesn't bundle into the main output still
// get uploaded alongside their sourcemaps
expect(contents).toContain(
'Content-Disposition: form-data; name="chunks/runtime.mjs"; filename="chunks/runtime.mjs"'
);
expect(contents).toContain(
'Content-Disposition: form-data; name="chunks/runtime.mjs.map"; filename="chunks/runtime.mjs.map"'
);
});

await runWrangler("pages deploy");
});
});
});

function mockGetProjectHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { Response } from "undici";
import { createWorkerUploadForm } from "../../deployment-bundle/create-worker-upload-form";
import { loadSourceMaps } from "../../deployment-bundle/source-maps";
import type { Config } from "../../config";
import type { BundleResult } from "../../deployment-bundle/bundle";
import type { CfPlacement, CfWorkerInit } from "../../deployment-bundle/worker";
Expand Down Expand Up @@ -86,7 +87,9 @@ function createWorkerBundleFormData(
keepVars: undefined,
keepSecrets: undefined,
logpush: undefined,
sourceMaps: undefined,
sourceMaps: config?.upload_source_maps
? loadSourceMaps(mainModule, workerBundle.modules, workerBundle)
: undefined,
placement: placement,
tail_consumers: undefined,
limits: config?.limits,
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/api/pages/deploy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export async function deploy({
workerBundle = await buildFunctions({
outputConfigPath,
functionsDirectory,
sourcemap: config?.upload_source_maps ?? false,
onEnd: () => {},
buildOutputDirectory: directory,
routesOutputPath,
Expand Down Expand Up @@ -309,6 +310,7 @@ export async function deploy({
buildOutputDirectory: directory,
nodejsCompat,
defineNavigatorUserAgent,
sourceMaps: config?.upload_source_maps ?? false,
});
} else if (_workerJS) {
if (bundle) {
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/config/validation-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const supportedPagesConfigFields = [
"browser",
// normalizeAndValidateConfig() sets this value
"configPath",
"upload_source_maps",
] as const;

export function validatePagesConfig(
Expand Down
Loading

0 comments on commit 0de4833

Please sign in to comment.