From 195970b4805841fd21b912e8ec64f5cfd614a617 Mon Sep 17 00:00:00 2001 From: Bryan Hoang Date: Fri, 20 Dec 2024 13:28:33 -0500 Subject: [PATCH] feat: add support for generating sourcemaps The CLI now accepts a `--sourcemap` option that should match the behaviour of `esbuild`'s `--sourcemap` option. The programmatic API's `esbuild.sourcemap` option should now also match with `esbuild`'s `sourcemap` option for the Build API. The `sourcemap: 'linked'` option is special cased in the implementation, since `esbuild` doesn't support the option under the Transform API that `mkdist` uses [^1]. Refs: https://esbuild.github.io/api/#sourcemap Refs: https://sourcemaps.info/spec.html [^1]: https://github.com/evanw/esbuild/blob/745abd9f0c06f73ca40fbe198546a9bc36c23b81/pkg/api/api_impl.go#L1749 --- src/cli.ts | 5 ++++ src/loaders/js.ts | 38 ++++++++++++++++++++++++--- test/index.test.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index a0909cd..041db7d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -85,6 +85,10 @@ const main = defineCommand({ type: "string", description: "Target environment (esbuild)", }, + sourcemap: { + type: "string", + description: "Emit sourcemap (esbuild)", + }, }, async run({ args }) { const { writtenFiles } = await mkdist({ @@ -103,6 +107,7 @@ const main = defineCommand({ jsxFragment: args.jsxFragment, minify: args.minify, target: args.target, + sourcemap: args.sourcemap, }, } as MkdistOptions); diff --git a/src/loaders/js.ts b/src/loaders/js.ts index e7e79fe..8f87b50 100644 --- a/src/loaders/js.ts +++ b/src/loaders/js.ts @@ -1,5 +1,6 @@ import { transform } from "esbuild"; import jiti from "jiti"; +import { basename, extname } from "pathe"; import type { Loader, LoaderResult } from "../loader"; @@ -18,6 +19,7 @@ export const jsLoader: Loader = async (input, { options }) => { const output: LoaderResult = []; let contents = await input.getContents(); + let sourceMapping = ""; // declaration if (options.declaration && !input.srcPath?.match(DECLARATION_RE)) { @@ -33,16 +35,28 @@ export const jsLoader: Loader = async (input, { options }) => { } // typescript => js + const sourcemap = + options.esbuild?.sourcemap === "linked" + ? "external" + : options.esbuild?.sourcemap; if (TS_EXTS.has(input.extension)) { - contents = await transform(contents, { + const result = await transform(contents, { ...options.esbuild, + sourcemap, + sourcefile: input.srcPath, loader: "ts", - }).then((r) => r.code); + }); + contents = result.code; + sourceMapping = result.map; } else if ([".tsx", ".jsx"].includes(input.extension)) { - contents = await transform(contents, { + const result = await transform(contents, { loader: input.extension === ".tsx" ? "tsx" : "jsx", ...options.esbuild, - }).then((r) => r.code); + sourcemap, + sourcefile: input.srcPath, + }); + contents = result.code; + sourceMapping = result.map; } // esm => cjs @@ -60,6 +74,22 @@ export const jsLoader: Loader = async (input, { options }) => { extension = options.ext.startsWith(".") ? options.ext : `.${options.ext}`; } + // sourcemap + if (options.esbuild?.sourcemap && sourceMapping !== "") { + if (options.esbuild.sourcemap !== "inline") { + output.push({ + contents: sourceMapping, + path: input.path, + extension: `${extension}.map`, + }); + } + + if (options.esbuild.sourcemap === "linked") { + const sourceMappingURL = `${basename(input.path, extname(input.path))}${extension}.map`; + contents += `\n//# sourceMappingURL=${sourceMappingURL}`; + } + } + output.push({ contents, path: input.path, diff --git a/test/index.test.ts b/test/index.test.ts index f2dc0dd..d0c5fbc 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -257,6 +257,71 @@ describe("mkdist", () => { `); }, 50_000); + it("mkdist (emit sourcemaps)", async () => { + const rootDir = resolve(__dirname, "fixture"); + const { writtenFiles } = await mkdist({ + rootDir, + esbuild: { + sourcemap: "linked", + }, + }); + + expect(writtenFiles.sort()).toEqual( + [ + "dist/README.md", + "dist/bar.mjs", + "dist/bar.mjs.map", + "dist/demo.css", + "dist/dir-export.mjs", + "dist/dir-export.mjs.map", + "dist/foo.mjs", + "dist/foo.d.ts", + "dist/index.mjs", + "dist/index.mjs.map", + "dist/types.d.ts", + "dist/star/index.mjs", + "dist/star/index.mjs.map", + "dist/star/other.mjs", + "dist/star/other.mjs.map", + "dist/components/index.mjs", + "dist/components/index.mjs.map", + "dist/components/blank.vue", + "dist/components/js.vue", + "dist/components/script-multi-block.vue", + "dist/components/script-multi-block.vue.mjs.map", + "dist/components/script-setup-ts.vue", + "dist/components/script-setup-ts.vue.mjs.map", + "dist/components/ts.vue", + "dist/components/ts.vue.mjs.map", + "dist/components/jsx.mjs", + "dist/components/jsx.mjs.map", + "dist/components/tsx.mjs", + "dist/components/tsx.mjs.map", + "dist/bar/index.mjs", + "dist/bar/index.mjs.map", + "dist/bar/esm.mjs", + "dist/ts/test1.mjs", + "dist/ts/test1.mjs.map", + "dist/ts/test2.mjs", + "dist/ts/test2.mjs.map", + "dist/nested.css", + "dist/prop-types/index.mjs", + "dist/prop-types/index.mjs.map", + ] + .map((f) => resolve(rootDir, f)) + .sort(), + ); + + expect(await readFile(resolve(rootDir, "dist/index.mjs"), "utf8")).toMatch( + /\n\/\/# sourceMappingURL=index.mjs.map$/, + ); + + expect( + JSON.parse(await readFile(resolve(rootDir, "dist/bar.mjs.map"), "utf8")) + .sourcesContent[0], + ).toMatch(await readFile(resolve(rootDir, "src/bar.ts"), "utf8")); + }); + describe("mkdist (sass compilation)", () => { const rootDir = resolve(__dirname, "fixture"); let writtenFiles: string[];