Skip to content

feat: Add rollup, esbuild, vite, rolldown, webpack and rspack bundler plugins #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 1, 2024
Merged
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
18 changes: 18 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: 'Build & Test'
on: [push, pull_request]

jobs:
build:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
- name: Install
run: yarn install
- name: Build
run: yarn build
- name: Run Unit Tests
run: yarn test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
.DS_Store
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,110 @@
# javascript-debug-ids

Bundler plugins to inject [Debug
IDs](https://github.com/tc39/source-map/blob/main/proposals/debug-id.md) into
both source and source-maps, making build artifacts self-identifying.

## Rollup

`rollup.config.mjs`
```ts
import debugIds from "@sentry/debug-ids/rollup";

export default {
input: "./src/main.js",
plugins: [debugIds()],
output: {
dir: "dist",
format: "esm",
sourcemap: true,
},
};
```

## webpack

`webpack.config.mjs`
```ts
import { DebugIdWebpackPlugin } from "@sentry/debug-ids/webpack";

export default {
entry: "./src/main.js",
plugins: [new DebugIdWebpackPlugin()],
mode: "production",
devtool: "source-map",
output: {
filename: "main.js",
path: "./dist",
},
};
```

## esbuild

`build.mjs`
```ts
import * as esbuild from "esbuild";
import debugIds from "@sentry/debug-ids/esbuild";

await esbuild.build({
entryPoints: ["./src/main.js"],
bundle: true,
format: "esm",
sourcemap: true,
minify: true,
plugins: [debugIds],
outdir: "./dist",
});
```

## vite

`vite.config.mjs`
```ts
import debugIds from "@sentry/debug-ids/vite";

export default {
root: "./src",
mode: "production",
plugins: [debugIds()],
build: {
outDir: "./dist",
sourcemap: true,
},
};
```

## rspack

`rspack.config.mjs`
```ts
import { DebugIdRspackPlugin } from "@sentry/debug-ids/rspack";

export default {
entry: "./src/main.js",
plugins: [new DebugIdRspackPlugin()],
mode: "production",
devtool: "source-map",
output: {
filename: "main.js",
path: "./dist",
},
};
```

## Rolldown

`rolldown.config.mjs`
```ts
import debugIds from "@sentry/debug-ids/rolldown";

export default {
input: "./src/main.js",
plugins: [debugIds()],
output: {
dir: "dist",
format: "esm",
sourcemap: true,
},
};
```
52 changes: 52 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@sentry/debug-ids",
"version": "0.0.1",
"license": "MIT",
"exports": {
"rollup": {
"require": "./dist/rollup.js",
"import": "./dist/rollup.mjs"
},
"webpack": {
"require": "./dist/webpack.js",
"import": "./dist/webpack.mjs"
},
"esbuild": {
"require": "./dist/esbuild.js",
"import": "./dist/esbuild.mjs"
},
"vite": {
"require": "./dist/vite.js",
"import": "./dist/vite.mjs"
},
"rspack": {
"require": "./dist/rspack.js",
"import": "./dist/rspack.mjs"
},
"rolldown": {
"require": "./dist/rolldown.js",
"import": "./dist/rolldown.mjs"
}
},
"scripts": {
"build": "pkgroll --clean-dist --target=es2018 --target=node14 --sourcemap",
"test": "vitest run ./test/**/*.test.ts"
},
"devDependencies": {
"@rspack/cli": "^1.0.8",
"@rspack/core": "^1.0.8",
"@types/node": "14",
"esbuild": "^0.24.0",
"pkgroll": "^2.5.0",
"rolldown": "^0.13.2",
"rollup": "^4.22.5",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"vitest": "^2.1.1",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4"
},
"volta": {
"node": "20.17.0"
}
}
41 changes: 41 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as crypto from "crypto";

export const DEFAULT_EXTENSIONS = [".js", ".mjs", ".cjs"];

export function stringToUUID(str: string): string {
const md5sum = crypto.createHash("md5");
md5sum.update(str);
const md5Hash = md5sum.digest("hex");

// Position 16 is fixed to either 8, 9, a, or b in the uuid v4 spec (10xx in binary)
// RFC 4122 section 4.4
const v4variant = ["8", "9", "a", "b"][
md5Hash.substring(16, 17).charCodeAt(0) % 4
] as string;

return (
md5Hash.substring(0, 8) +
"-" +
md5Hash.substring(8, 12) +
"-4" +
md5Hash.substring(13, 16) +
"-" +
v4variant +
md5Hash.substring(17, 20) +
"-" +
md5Hash.substring(20)
).toLowerCase();
}

export function addDebugIdToSource(input: string, debugId: string): string {
return input.replace(
/\s*(?:\/\/# debugId=.+)?\s*(\/\/# sourceMappingURL=.+)?\s*$/,
`\n//# debugId=${debugId}\n$1`
);
}

export function addDebugIdToSourcemap(input: string, debugId: string): string {
const sourceMapObj = JSON.parse(input);
sourceMapObj.debugId = debugId;
return JSON.stringify(sourceMapObj);
}
56 changes: 56 additions & 0 deletions src/esbuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { PluginBuild } from "esbuild";
import {
addDebugIdToSource,
DEFAULT_EXTENSIONS,
stringToUUID,
addDebugIdToSourcemap,
} from "./common";
import { readFile, writeFile } from "fs/promises";
import * as path from "path";

export default {
name: "esbuild-plugin-debug-ids",
setup({ onEnd, initialOptions }: PluginBuild) {
if (
initialOptions.bundle !== true ||
initialOptions.sourcemap === undefined
) {
return;
}

initialOptions.metafile = true;

onEnd(async (result) => {
const assets = Object.keys(result.metafile?.outputs || {});
const assetsWithCorrectExt = assets.filter((file) =>
DEFAULT_EXTENSIONS.some((ext) => file.endsWith(ext))
);

if (assetsWithCorrectExt.length === 0) {
return;
}

const promises = assetsWithCorrectExt.map(async (key) => {
const sourcemapKey = `${key}.map`;

// If we don't have a sourcemap, don't do anything
if (!assets.includes(sourcemapKey)) {
return Promise.resolve();
}

const sourcePath = path.join(process.cwd(), key);
const source = await readFile(sourcePath, { encoding: "utf-8" });
const debugId = stringToUUID(source);
const updatedSource = addDebugIdToSource(source, debugId);
await writeFile(sourcePath, updatedSource);

const sourcemapPath = path.join(process.cwd(), sourcemapKey);
const sourcemap = await readFile(sourcemapPath, { encoding: "utf-8" });
const updatedSourcemap = addDebugIdToSourcemap(sourcemap, debugId);
await writeFile(sourcemapPath, updatedSourcemap);
});

await Promise.all(promises);
});
},
};
3 changes: 3 additions & 0 deletions src/rolldown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import rollupPlugin from "./rollup";

export default rollupPlugin;
58 changes: 58 additions & 0 deletions src/rollup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { OutputAsset, OutputChunk, Plugin } from "rollup";
import {
addDebugIdToSourcemap,
addDebugIdToSource,
stringToUUID,
DEFAULT_EXTENSIONS,
} from "./common";

export default function debugIds(): Plugin {
return {
name: "rollup-plugin-debug-ids",
generateBundle: function (
_,
bundle: { [fileName: string]: OutputAsset | OutputChunk }
) {
for (const [key, value] of Object.entries(bundle)) {
// We only add debugId where there is a linked sourcemap file
if (!("sourcemapFileName" in value) || !value.sourcemapFileName) {
continue;
}

// We only add to specific file types
if (!DEFAULT_EXTENSIONS.includes(key.slice(-3))) {
continue;
}

// Check we have a sourcemap in the output and it has source property
const sourceMapFile = bundle[value.sourcemapFileName];
if (!sourceMapFile || !("source" in sourceMapFile)) {
continue;
}

const debugId = stringToUUID(value.code);
value.code = addDebugIdToSource(value.code, debugId);
sourceMapFile.source = addDebugIdToSourcemap(
sourceMapFile.source.toString(),
debugId
);

// vite has plugins that run after us which can modify the sourcemap so we
// proxy the sourceMapFile to re-add the debugId if the source gets set again
bundle[value.sourcemapFileName] = new Proxy(
bundle[value.sourcemapFileName],
{
set: function (target, prop, value) {
if (prop === "source") {
target[prop] = addDebugIdToSourcemap(value, debugId);
} else {
target[prop] = value;
}
return true;
},
}
);
}
},
};
}
3 changes: 3 additions & 0 deletions src/rspack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { DebugIdWebpackPlugin } from "./webpack";

export class DebugIdRspackPlugin extends DebugIdWebpackPlugin {}
11 changes: 11 additions & 0 deletions src/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import rollupPlugin from "./rollup";
import { Plugin } from "vite";

export default function debugIds(): Plugin {
return {
...rollupPlugin(),
name: "vite-plugin-debug-ids",
apply: "build",
enforce: "post",
};
}
Loading