mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## Summary - Adds `--metafile-md` CLI option to `bun build` that generates a markdown visualization of the module graph - Designed to help Claude and other LLMs analyze bundle composition, identify bloat, and understand dependency chains - Reuses existing metafile JSON generation code as a post-processing step ## Features The generated markdown includes: 1. **Quick Summary** - Module counts, sizes, ESM/CJS breakdown, output/input ratio 2. **Largest Input Files** - Sorted by size to identify potential bloat 3. **Entry Point Analysis** - Shows bundle size, exports, CSS bundles, and bundled modules 4. **Dependency Chains** - Most commonly imported modules and reverse dependencies 5. **Full Module Graph** - Complete import/export info for each module 6. **Raw Data for Searching** - Grep-friendly markers in code blocks: - `[MODULE:]`, `[SIZE:]`, `[IMPORT:]`, `[IMPORTED_BY:]` - `[ENTRY:]`, `[EXTERNAL:]`, `[NODE_MODULES:]` ## Usage ```bash # Default filename (meta.md) bun build entry.js --metafile-md --outdir=dist # Custom filename bun build entry.js --metafile-md=analysis.md --outdir=dist # Both JSON and markdown bun build entry.js --metafile=meta.json --metafile-md=meta.md --outdir=dist ``` ## Example Output See sample output: https://gist.github.com/example (will add) ## Test plan - [x] Test default filename (`meta.md`) - [x] Test custom filename - [x] Test both `--metafile` and `--metafile-md` together - [x] Test summary metrics - [x] Test module format info (ESM/CJS) - [x] Test external imports - [x] Test exports list - [x] Test bundled modules table - [x] Test CSS bundle reference - [x] Test import kinds (static, dynamic, require) - [x] Test commonly imported modules - [x] Test largest files sorting (bloat analysis) - [x] Test output/input ratio - [x] Test grep-friendly raw data section - [x] Test entry point markers - [x] Test external import markers - [x] Test node_modules markers All 17 new tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
1204 lines
38 KiB
TypeScript
1204 lines
38 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { tempDir } from "harness";
|
|
|
|
// Type definitions for metafile structure
|
|
interface MetafileImport {
|
|
path: string;
|
|
kind: string;
|
|
original?: string;
|
|
external?: boolean;
|
|
with?: { type: string };
|
|
}
|
|
|
|
interface MetafileInput {
|
|
bytes: number;
|
|
imports: MetafileImport[];
|
|
format?: "esm" | "cjs";
|
|
}
|
|
|
|
interface MetafileOutput {
|
|
bytes: number;
|
|
inputs: Record<string, { bytesInOutput: number }>;
|
|
imports: Array<{ path: string; kind: string; external?: boolean }>;
|
|
exports: string[];
|
|
entryPoint?: string;
|
|
cssBundle?: string;
|
|
}
|
|
|
|
interface Metafile {
|
|
inputs: Record<string, MetafileInput>;
|
|
outputs: Record<string, MetafileOutput>;
|
|
}
|
|
|
|
describe("bundler metafile", () => {
|
|
test("metafile option returns metafile object", async () => {
|
|
using dir = tempDir("metafile-test", {
|
|
"index.js": `import { foo } from "./foo.js"; console.log(foo);`,
|
|
"foo.js": `export const foo = "hello";`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/index.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
expect(typeof result.metafile).toBe("object");
|
|
|
|
// Check metafile structure - metafile returns the JSON directly (backward compatible with esbuild)
|
|
const metafile = result.metafile as Metafile;
|
|
expect(metafile.inputs).toBeDefined();
|
|
expect(typeof metafile.inputs).toBe("object");
|
|
expect(metafile.outputs).toBeDefined();
|
|
expect(typeof metafile.outputs).toBe("object");
|
|
});
|
|
|
|
test("metafile inputs contain file metadata", async () => {
|
|
using dir = tempDir("metafile-inputs-test", {
|
|
"entry.js": `import { helper } from "./helper.js"; helper();`,
|
|
"helper.js": `export function helper() { return 42; }`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
const inputKeys = Object.keys(inputs);
|
|
|
|
// Should have at least 2 input files
|
|
expect(inputKeys.length).toBeGreaterThanOrEqual(2);
|
|
|
|
// Each input should have bytes and imports
|
|
for (const key of inputKeys) {
|
|
const input = inputs[key];
|
|
expect(typeof input.bytes).toBe("number");
|
|
expect(input.bytes).toBeGreaterThan(0);
|
|
expect(Array.isArray(input.imports)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("metafile outputs contain chunk metadata", async () => {
|
|
using dir = tempDir("metafile-outputs-test", {
|
|
"main.js": `export const value = 123;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/main.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const outputs = (result.metafile as Metafile).outputs as Record<string, MetafileOutput>;
|
|
const outputKeys = Object.keys(outputs);
|
|
|
|
// Should have at least 1 output
|
|
expect(outputKeys.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// Each output should have bytes, inputs, imports, exports
|
|
for (const key of outputKeys) {
|
|
const output = outputs[key];
|
|
expect(typeof output.bytes).toBe("number");
|
|
expect(typeof output.inputs).toBe("object");
|
|
expect(Array.isArray(output.imports)).toBe(true);
|
|
expect(Array.isArray(output.exports)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("metafile tracks import relationships", async () => {
|
|
using dir = tempDir("metafile-imports-test", {
|
|
"index.js": `import { a } from "./a.js"; console.log(a);`,
|
|
"a.js": `import { b } from "./b.js"; export const a = b + 1;`,
|
|
"b.js": `export const b = 10;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/index.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
// Find the entry file in inputs
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
let entryInput: MetafileInput | null = null;
|
|
for (const [path, input] of Object.entries(inputs)) {
|
|
if (path.includes("index.js")) {
|
|
entryInput = input;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(entryInput).not.toBeNull();
|
|
// Entry should have an import to a.js
|
|
expect(entryInput!.imports.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("metafile imports have resolved path and original specifier", async () => {
|
|
using dir = tempDir("metafile-resolved-path-test", {
|
|
"entry.js": `import { foo } from "./lib/helper.js"; console.log(foo);`,
|
|
"lib/helper.js": `export const foo = 42;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
// Find the entry file in inputs
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
let entryImports: MetafileImport[] | null = null;
|
|
for (const [path, input] of Object.entries(inputs)) {
|
|
if (path.includes("entry.js")) {
|
|
entryImports = input.imports;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(entryImports).not.toBeNull();
|
|
expect(entryImports!.length).toBe(1);
|
|
|
|
const imp = entryImports![0];
|
|
// path should be the resolved path (contains lib/helper.js or lib\helper.js on Windows)
|
|
expect(imp.path.includes("lib/helper.js") || imp.path.includes("lib\\helper.js")).toBe(true);
|
|
expect(imp.kind).toBe("import-statement");
|
|
// original should be the original import specifier
|
|
expect(imp.original).toBe("./lib/helper.js");
|
|
});
|
|
|
|
test("metafile without option returns undefined", async () => {
|
|
using dir = tempDir("metafile-disabled-test", {
|
|
"test.js": `console.log("test");`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/test.js`],
|
|
// metafile is not set (defaults to false)
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeUndefined();
|
|
});
|
|
|
|
test("metafile tracks exports", async () => {
|
|
using dir = tempDir("metafile-exports-test", {
|
|
"lib.js": `export const foo = 1; export const bar = 2; export default function() {}`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/lib.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const outputs = (result.metafile as Metafile).outputs as Record<string, MetafileOutput>;
|
|
const outputKeys = Object.keys(outputs);
|
|
expect(outputKeys.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// Find the main output
|
|
const mainOutput = outputs[outputKeys[0]];
|
|
expect(mainOutput.exports).toBeDefined();
|
|
expect(Array.isArray(mainOutput.exports)).toBe(true);
|
|
});
|
|
|
|
test("metafile includes entryPoint for entry chunks", async () => {
|
|
using dir = tempDir("metafile-entrypoint-test", {
|
|
"entry.js": `console.log("entry");`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const outputs = (result.metafile as Metafile).outputs as Record<string, MetafileOutput>;
|
|
const outputKeys = Object.keys(outputs);
|
|
|
|
// At least one output should have entryPoint
|
|
let hasEntryPoint = false;
|
|
for (const key of outputKeys) {
|
|
if (outputs[key].entryPoint) {
|
|
hasEntryPoint = true;
|
|
expect(typeof outputs[key].entryPoint).toBe("string");
|
|
break;
|
|
}
|
|
}
|
|
expect(hasEntryPoint).toBe(true);
|
|
});
|
|
|
|
test("metafile includes format for JS inputs", async () => {
|
|
using dir = tempDir("metafile-format-test", {
|
|
"esm.js": `export const x = 1;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/esm.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
// At least one input should have format
|
|
let hasFormat = false;
|
|
for (const key of Object.keys(inputs)) {
|
|
if (inputs[key].format) {
|
|
hasFormat = true;
|
|
expect(["esm", "cjs"]).toContain(inputs[key].format);
|
|
break;
|
|
}
|
|
}
|
|
expect(hasFormat).toBe(true);
|
|
});
|
|
|
|
test("metafile detects cjs format for CommonJS files", async () => {
|
|
using dir = tempDir("metafile-cjs-format-test", {
|
|
"entry.js": `const foo = require("./foo.js"); console.log(foo);`,
|
|
"foo.js": `module.exports = { value: 42 };`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
// Find the foo.js file which uses CommonJS exports
|
|
let fooInput: MetafileInput | null = null;
|
|
for (const [path, input] of Object.entries(inputs)) {
|
|
if (path.includes("foo.js")) {
|
|
fooInput = input;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(fooInput).not.toBeNull();
|
|
expect(fooInput!.format).toBe("cjs");
|
|
});
|
|
|
|
test("metafile marks external imports", async () => {
|
|
using dir = tempDir("metafile-external-test", {
|
|
"index.js": `import fs from "fs"; console.log(fs);`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/index.js`],
|
|
metafile: true,
|
|
external: ["fs"],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
let foundExternal = false;
|
|
|
|
for (const key of Object.keys(inputs)) {
|
|
const input = inputs[key];
|
|
for (const imp of input.imports) {
|
|
if (imp.path === "fs" && imp.external === true) {
|
|
foundExternal = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(foundExternal).toBe(true);
|
|
});
|
|
|
|
test("metafile with code splitting", async () => {
|
|
using dir = tempDir("metafile-splitting-test", {
|
|
"a.js": `import { shared } from "./shared.js"; console.log("a", shared);`,
|
|
"b.js": `import { shared } from "./shared.js"; console.log("b", shared);`,
|
|
"shared.js": `export const shared = "shared value";`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/a.js`, `${dir}/b.js`],
|
|
metafile: true,
|
|
splitting: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const outputs = (result.metafile as Metafile).outputs as Record<string, MetafileOutput>;
|
|
const outputKeys = Object.keys(outputs);
|
|
|
|
// With splitting, we should have more outputs (shared chunk)
|
|
expect(outputKeys.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
test("metafile includes with clause for JSON imports", async () => {
|
|
using dir = tempDir("metafile-with-json-test", {
|
|
"entry.js": `import data from "./data.json"; console.log(data);`,
|
|
"data.json": `{"key": "value"}`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
// Find the entry file in inputs
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
let jsonImport: MetafileImport | null = null;
|
|
for (const [path, input] of Object.entries(inputs)) {
|
|
if (path.includes("entry.js")) {
|
|
for (const imp of input.imports) {
|
|
if (imp.path.includes("data.json")) {
|
|
jsonImport = imp;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(jsonImport).not.toBeNull();
|
|
expect(jsonImport!.with).toBeDefined();
|
|
expect(jsonImport!.with!.type).toBe("json");
|
|
});
|
|
|
|
test("metafile tracks require-call imports", async () => {
|
|
using dir = tempDir("metafile-require-test", {
|
|
"entry.js": `const foo = require("./foo.js"); console.log(foo);`,
|
|
"foo.js": `module.exports = { value: 42 };`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
// Find the entry file in inputs
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
let requireImport: MetafileImport | null = null;
|
|
for (const [path, input] of Object.entries(inputs)) {
|
|
if (path.includes("entry.js")) {
|
|
for (const imp of input.imports) {
|
|
if (imp.path.includes("foo.js")) {
|
|
requireImport = imp;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(requireImport).not.toBeNull();
|
|
expect(requireImport!.kind).toBe("require-call");
|
|
});
|
|
|
|
test("metafile tracks dynamic-import imports", async () => {
|
|
using dir = tempDir("metafile-dynamic-import-test", {
|
|
"entry.js": `import("./dynamic.js").then(m => console.log(m));`,
|
|
"dynamic.js": `export const value = 123;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
splitting: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
// Find the entry file in inputs
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
let dynamicImport: MetafileImport | null = null;
|
|
for (const [path, input] of Object.entries(inputs)) {
|
|
if (path.includes("entry.js")) {
|
|
for (const imp of input.imports) {
|
|
if (imp.kind === "dynamic-import" && imp.original === "./dynamic.js") {
|
|
dynamicImport = imp;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(dynamicImport).not.toBeNull();
|
|
expect(dynamicImport!.kind).toBe("dynamic-import");
|
|
expect(dynamicImport!.original).toBe("./dynamic.js");
|
|
// The path should be the final chunk path (e.g., "./chunk-xxx.js"), not the internal unique_key
|
|
expect(dynamicImport!.path).toMatch(/^\.\/chunk-[a-z0-9]+\.js$/);
|
|
|
|
// Verify the path corresponds to an actual output chunk
|
|
const outputs = (result.metafile as Metafile).outputs as Record<string, MetafileOutput>;
|
|
const outputPaths = Object.keys(outputs);
|
|
expect(outputPaths).toContain(dynamicImport!.path);
|
|
});
|
|
|
|
test("metafile includes cssBundle for CSS outputs", async () => {
|
|
using dir = tempDir("metafile-css-bundle-test", {
|
|
"entry.js": `import "./styles.css"; console.log("styled");`,
|
|
"styles.css": `.foo { color: red; }`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const outputs = (result.metafile as Metafile).outputs as Record<string, MetafileOutput>;
|
|
|
|
// Find the JS output that should reference the CSS bundle
|
|
let foundCssBundle = false;
|
|
for (const [outputPath, output] of Object.entries(outputs)) {
|
|
if (outputPath.endsWith(".js") && output.cssBundle) {
|
|
foundCssBundle = true;
|
|
expect(typeof output.cssBundle).toBe("string");
|
|
expect(output.cssBundle.endsWith(".css")).toBe(true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(foundCssBundle).toBe(true);
|
|
});
|
|
|
|
test("metafile handles circular imports", async () => {
|
|
using dir = tempDir("metafile-circular-test", {
|
|
"a.js": `import { b } from "./b.js"; export const a = 1; console.log(b);`,
|
|
"b.js": `import { a } from "./a.js"; export const b = 2; console.log(a);`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/a.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
const inputs = (result.metafile as Metafile).inputs as Record<string, MetafileInput>;
|
|
const inputKeys = Object.keys(inputs);
|
|
|
|
// Should have both files
|
|
expect(inputKeys.length).toBe(2);
|
|
|
|
// Both files should have imports to each other
|
|
let aImportsB = false;
|
|
let bImportsA = false;
|
|
for (const [path, input] of Object.entries(inputs)) {
|
|
if (path.includes("a.js")) {
|
|
aImportsB = input.imports.some(imp => imp.path.includes("b.js"));
|
|
}
|
|
if (path.includes("b.js")) {
|
|
bImportsA = input.imports.some(imp => imp.path.includes("a.js"));
|
|
}
|
|
}
|
|
|
|
expect(aImportsB).toBe(true);
|
|
expect(bImportsA).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Bun.build metafile option variants", () => {
|
|
test("metafile: string writes JSON to file path", async () => {
|
|
using dir = tempDir("metafile-string-path", {
|
|
"entry.js": `import { foo } from "./foo.js"; console.log(foo);`,
|
|
"foo.js": `export const foo = "hello";`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
outdir: `${dir}/dist`,
|
|
metafile: "output-meta.json",
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Check JSON file was written (relative to outdir)
|
|
const jsonFile = Bun.file(`${dir}/dist/output-meta.json`);
|
|
expect(await jsonFile.exists()).toBe(true);
|
|
|
|
// Verify JSON content
|
|
const content = await jsonFile.text();
|
|
const parsed = JSON.parse(content);
|
|
expect(parsed.inputs).toBeDefined();
|
|
expect(parsed.outputs).toBeDefined();
|
|
|
|
// Also check result.metafile is available (backward compatible - returns JSON directly)
|
|
expect(result.metafile).toBeDefined();
|
|
const metafile = result.metafile as Metafile;
|
|
expect(typeof metafile).toBe("object");
|
|
expect(metafile.inputs).toBeDefined();
|
|
expect(metafile.outputs).toBeDefined();
|
|
});
|
|
|
|
test("metafile: { json: path } writes JSON to specified path", async () => {
|
|
using dir = tempDir("metafile-object-json", {
|
|
"main.js": `export const value = 42;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/main.js`],
|
|
outdir: `${dir}/dist`,
|
|
metafile: { json: "custom-meta.json" },
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Check JSON file was written (relative to outdir)
|
|
const jsonFile = Bun.file(`${dir}/dist/custom-meta.json`);
|
|
expect(await jsonFile.exists()).toBe(true);
|
|
|
|
// Verify content
|
|
const parsed = JSON.parse(await jsonFile.text());
|
|
expect(parsed.inputs).toBeDefined();
|
|
expect(parsed.outputs).toBeDefined();
|
|
});
|
|
|
|
test("metafile: { markdown: path } writes markdown to specified path", async () => {
|
|
using dir = tempDir("metafile-object-md", {
|
|
"app.js": `import "./util.js"; console.log("app");`,
|
|
"util.js": `console.log("util");`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/app.js`],
|
|
outdir: `${dir}/dist`,
|
|
metafile: { markdown: "analysis.md" },
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Check markdown file was written (relative to outdir)
|
|
const mdFile = Bun.file(`${dir}/dist/analysis.md`);
|
|
expect(await mdFile.exists()).toBe(true);
|
|
|
|
// Verify markdown content
|
|
const content = await mdFile.text();
|
|
expect(content).toContain("# Bundle Analysis Report");
|
|
expect(content).toContain("app.js");
|
|
|
|
// Also check result.metafile is available (backward compatible - returns JSON directly)
|
|
expect(result.metafile).toBeDefined();
|
|
const metafile = result.metafile as Metafile;
|
|
expect(metafile.inputs).toBeDefined();
|
|
});
|
|
|
|
test("metafile: { json: path, markdown: path } writes both files", async () => {
|
|
using dir = tempDir("metafile-object-both", {
|
|
"index.js": `import { helper } from "./helper.js"; helper();`,
|
|
"helper.js": `export function helper() { return "help"; }`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/index.js`],
|
|
outdir: `${dir}/dist`,
|
|
metafile: {
|
|
json: "meta.json",
|
|
markdown: "meta.md",
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Check both files exist (relative to outdir)
|
|
const jsonFile = Bun.file(`${dir}/dist/meta.json`);
|
|
const mdFile = Bun.file(`${dir}/dist/meta.md`);
|
|
expect(await jsonFile.exists()).toBe(true);
|
|
expect(await mdFile.exists()).toBe(true);
|
|
|
|
// Verify JSON
|
|
const parsedJson = JSON.parse(await jsonFile.text());
|
|
expect(parsedJson.inputs).toBeDefined();
|
|
|
|
// Verify markdown
|
|
const mdContent = await mdFile.text();
|
|
expect(mdContent).toContain("# Bundle Analysis Report");
|
|
|
|
// result.metafile is available (backward compatible - returns JSON directly)
|
|
expect(result.metafile).toBeDefined();
|
|
const metafile = result.metafile as Metafile;
|
|
expect(metafile.inputs).toBeDefined();
|
|
expect(metafile.outputs).toBeDefined();
|
|
});
|
|
|
|
test("metafile is lazily parsed", async () => {
|
|
using dir = tempDir("metafile-lazy-json", {
|
|
"entry.js": `export const x = 1;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/entry.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
|
|
// First access should parse the JSON (backward compatible - returns JSON directly)
|
|
const metafile1 = result.metafile as Metafile;
|
|
expect(metafile1).toBeDefined();
|
|
expect(typeof metafile1).toBe("object");
|
|
expect(metafile1.inputs).toBeDefined();
|
|
|
|
// Second access should return the same cached object
|
|
const metafile2 = result.metafile as Metafile;
|
|
expect(metafile1).toBe(metafile2); // Same reference (memoized)
|
|
});
|
|
|
|
test("metafile: true provides metafile object", async () => {
|
|
using dir = tempDir("metafile-true", {
|
|
"test.js": `console.log("test");`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/test.js`],
|
|
metafile: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
// Backward compatible - returns JSON directly
|
|
const metafile = result.metafile as Metafile;
|
|
expect(metafile.inputs).toBeDefined();
|
|
expect(metafile.outputs).toBeDefined();
|
|
});
|
|
|
|
test("metafile: { markdown: path } provides metafile object", async () => {
|
|
using dir = tempDir("metafile-md-has-json", {
|
|
"test.js": `export const a = 1;`,
|
|
});
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [`${dir}/test.js`],
|
|
outdir: `${dir}/dist`,
|
|
metafile: { markdown: "meta.md" },
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.metafile).toBeDefined();
|
|
// Backward compatible - returns JSON directly
|
|
const metafile = result.metafile as Metafile;
|
|
expect(metafile.inputs).toBeDefined();
|
|
expect(metafile.outputs).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// CLI tests for --metafile-md
|
|
import { bunEnv, bunExe } from "harness";
|
|
|
|
describe("bun build --metafile-md", () => {
|
|
test("generates markdown metafile with default name", async () => {
|
|
using dir = tempDir("metafile-md-test", {
|
|
"index.js": `import { foo } from "./foo.js"; console.log(foo);`,
|
|
"foo.js": `export const foo = "hello";`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "index.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Check meta.md was created
|
|
const metaFile = Bun.file(`${dir}/meta.md`);
|
|
expect(await metaFile.exists()).toBe(true);
|
|
|
|
const content = await metaFile.text();
|
|
|
|
// Verify markdown structure
|
|
expect(content).toContain("# Bundle Analysis Report");
|
|
expect(content).toContain("## Quick Summary");
|
|
expect(content).toContain("## Entry Point Analysis");
|
|
expect(content).toContain("## Full Module Graph");
|
|
|
|
// Verify content includes our files
|
|
expect(content).toContain("index.js");
|
|
expect(content).toContain("foo.js");
|
|
});
|
|
|
|
test("generates markdown metafile with custom name", async () => {
|
|
using dir = tempDir("metafile-md-custom-name", {
|
|
"main.js": `export const value = 42;`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "main.js", "--metafile-md=build-graph.md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Check custom-named file was created
|
|
const metaFile = Bun.file(`${dir}/build-graph.md`);
|
|
expect(await metaFile.exists()).toBe(true);
|
|
|
|
const content = await metaFile.text();
|
|
expect(content).toContain("# Bundle Analysis Report");
|
|
expect(content).toContain("main.js");
|
|
});
|
|
|
|
test("generates both metafile and metafile-md when both specified", async () => {
|
|
using dir = tempDir("metafile-both", {
|
|
"app.js": `import "./util.js"; console.log("app");`,
|
|
"util.js": `console.log("util");`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "app.js", "--metafile=meta.json", "--metafile-md=meta.md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Check both files exist
|
|
const jsonFile = Bun.file(`${dir}/meta.json`);
|
|
const mdFile = Bun.file(`${dir}/meta.md`);
|
|
|
|
expect(await jsonFile.exists()).toBe(true);
|
|
expect(await mdFile.exists()).toBe(true);
|
|
|
|
// Verify JSON is valid
|
|
const jsonContent = await jsonFile.text();
|
|
const parsed = JSON.parse(jsonContent);
|
|
expect(parsed.inputs).toBeDefined();
|
|
expect(parsed.outputs).toBeDefined();
|
|
|
|
// Verify markdown structure
|
|
const mdContent = await mdFile.text();
|
|
expect(mdContent).toContain("# Bundle Analysis Report");
|
|
});
|
|
|
|
test("markdown includes summary metrics", async () => {
|
|
using dir = tempDir("metafile-md-metrics", {
|
|
"entry.js": `import { a } from "./a.js"; import { b } from "./b.js"; console.log(a, b);`,
|
|
"a.js": `export const a = 1;`,
|
|
"b.js": `export const b = 2;`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "entry.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Verify summary table
|
|
expect(content).toContain("| Input modules |");
|
|
expect(content).toContain("| Entry points |");
|
|
expect(content).toContain("| Total output size |");
|
|
expect(content).toContain("| ESM modules |");
|
|
});
|
|
|
|
test("markdown includes module format information", async () => {
|
|
using dir = tempDir("metafile-md-format", {
|
|
"esm.js": `export const x = 1;`,
|
|
"cjs.js": `module.exports = { y: 2 };`,
|
|
"entry.js": `import { x } from "./esm.js"; const cjs = require("./cjs.js"); console.log(x, cjs);`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "entry.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Should indicate both esm and cjs formats
|
|
expect(content).toContain("**Format**: esm");
|
|
expect(content).toContain("**Format**: cjs");
|
|
});
|
|
|
|
test("markdown includes external imports", async () => {
|
|
using dir = tempDir("metafile-md-external", {
|
|
"app.js": `import fs from "fs"; console.log(fs);`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "app.js", "--metafile-md", "--external=fs", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Check external is noted in summary
|
|
expect(content).toContain("External imports");
|
|
|
|
// Check external marker in imports list
|
|
expect(content).toContain("**external**");
|
|
});
|
|
|
|
test("markdown includes exports list", async () => {
|
|
using dir = tempDir("metafile-md-exports", {
|
|
"lib.js": `export const foo = 1; export const bar = 2; export default function main() {}`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "lib.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Check exports are listed
|
|
expect(content).toContain("**Exports**:");
|
|
expect(content).toContain("`foo`");
|
|
expect(content).toContain("`bar`");
|
|
expect(content).toContain("`default`");
|
|
});
|
|
|
|
test("markdown includes bundled modules table", async () => {
|
|
using dir = tempDir("metafile-md-bundled", {
|
|
"index.js": `import { utils } from "./utils.js"; utils();`,
|
|
"utils.js": `export function utils() { return "utility"; }`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "index.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Check bundled modules table
|
|
expect(content).toContain("**Bundled modules**");
|
|
expect(content).toContain("| Bytes | Module |");
|
|
});
|
|
|
|
test("markdown includes CSS bundle reference", async () => {
|
|
using dir = tempDir("metafile-md-css", {
|
|
"app.js": `import "./styles.css"; console.log("styled");`,
|
|
"styles.css": `.foo { color: red; }`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "app.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Check CSS bundle reference
|
|
expect(content).toContain("**CSS bundle**:");
|
|
expect(content).toContain(".css");
|
|
});
|
|
|
|
test("markdown includes import kinds", async () => {
|
|
using dir = tempDir("metafile-md-import-kinds", {
|
|
"entry.js": `
|
|
import { static_import } from "./static.js";
|
|
const dynamic = import("./dynamic.js");
|
|
const required = require("./required.js");
|
|
`,
|
|
"static.js": `export const static_import = 1;`,
|
|
"dynamic.js": `export const dynamic_value = 2;`,
|
|
"required.js": `module.exports = { required_value: 3 };`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "entry.js", "--metafile-md", "--outdir=dist", "--splitting"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Check import kinds are shown
|
|
expect(content).toContain("import-statement");
|
|
expect(content).toContain("dynamic-import");
|
|
expect(content).toContain("require-call");
|
|
});
|
|
|
|
test("markdown shows commonly imported modules", async () => {
|
|
using dir = tempDir("metafile-md-common-imports", {
|
|
"a.js": `import { shared } from "./shared.js"; console.log("a", shared);`,
|
|
"b.js": `import { shared } from "./shared.js"; console.log("b", shared);`,
|
|
"c.js": `import { shared } from "./shared.js"; console.log("c", shared);`,
|
|
"shared.js": `export const shared = "common code";`,
|
|
"entry.js": `import "./a.js"; import "./b.js"; import "./c.js";`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "entry.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Verify the Dependency Chains section exists
|
|
expect(content).toContain("## Dependency Chains");
|
|
expect(content).toContain("Most Commonly Imported Modules");
|
|
|
|
// shared.js should be listed as commonly imported (by 3 files)
|
|
expect(content).toContain("shared.js");
|
|
|
|
// Should show imported by a.js, b.js, c.js
|
|
expect(content).toContain("a.js");
|
|
expect(content).toContain("b.js");
|
|
expect(content).toContain("c.js");
|
|
});
|
|
|
|
test("markdown shows largest files for bloat analysis", async () => {
|
|
using dir = tempDir("metafile-md-bloat", {
|
|
"entry.js": `import "./small.js"; import "./large.js";`,
|
|
"small.js": `export const s = 1;`,
|
|
"large.js": `export const large = "${"x".repeat(500)}";`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "entry.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Verify bloat analysis section
|
|
expect(content).toContain("## Largest Modules by Output Contribution");
|
|
expect(content).toContain("bytes contributed to the output bundle");
|
|
expect(content).toContain("% of Total");
|
|
});
|
|
|
|
test("markdown shows output contribution", async () => {
|
|
using dir = tempDir("metafile-md-contrib", {
|
|
"entry.js": `export const x = 1;`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "entry.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Should show output contribution in Full Module Graph
|
|
expect(content).toContain("**Output contribution**:");
|
|
expect(content).toMatch(/\d+\.\d+%/); // Should have percentage in Largest Modules section
|
|
});
|
|
|
|
test("markdown includes grep-friendly raw data section", async () => {
|
|
using dir = tempDir("metafile-md-grep", {
|
|
"main.js": `import { helper } from "./helper.js"; console.log(helper);`,
|
|
"helper.js": `export const helper = "utility";`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "main.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Should have table of contents
|
|
expect(content).toContain("## Table of Contents");
|
|
expect(content).toContain("[Quick Summary]");
|
|
expect(content).toContain("[Raw Data for Searching]");
|
|
|
|
// Should have raw data section
|
|
expect(content).toContain("## Raw Data for Searching");
|
|
|
|
// Should have grep-friendly markers
|
|
expect(content).toContain("[MODULE:");
|
|
expect(content).toContain("[OUTPUT_BYTES:");
|
|
expect(content).toContain("[IMPORT:");
|
|
expect(content).toContain("[IMPORTED_BY:");
|
|
|
|
// main.js imports helper.js should be searchable
|
|
expect(content).toMatch(/\[IMPORT: main\.js -> .*helper\.js\]/);
|
|
|
|
// helper.js is imported by main.js
|
|
expect(content).toMatch(/\[IMPORTED_BY: .*helper\.js <- main\.js\]/);
|
|
});
|
|
|
|
test("markdown includes entry point markers", async () => {
|
|
using dir = tempDir("metafile-md-entry-markers", {
|
|
"app.js": `console.log("app");`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "app.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Should have entry point marker in raw data
|
|
expect(content).toContain("[ENTRY:");
|
|
// Entry format is: [ENTRY: source -> output (bytes)]
|
|
expect(content).toMatch(/\[ENTRY: app\.js -> .*app\.js/);
|
|
});
|
|
|
|
test("markdown includes external import markers", async () => {
|
|
using dir = tempDir("metafile-md-external-markers", {
|
|
"index.js": `import fs from "fs"; console.log(fs);`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "index.js", "--metafile-md", "--external=fs", "--outdir=dist", "--target=node"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Should have external marker in raw data
|
|
expect(content).toContain("[EXTERNAL:");
|
|
expect(content).toMatch(/\[EXTERNAL: index\.js imports fs\]/);
|
|
});
|
|
|
|
test("markdown includes node_modules markers", async () => {
|
|
using dir = tempDir("metafile-md-node-modules", {
|
|
"app.js": `import lodash from "./node_modules/lodash/index.js"; console.log(lodash);`,
|
|
"node_modules/lodash/index.js": `export default { version: "4.0.0" };`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "build", "app.js", "--metafile-md", "--outdir=dist"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
const content = await Bun.file(`${dir}/meta.md`).text();
|
|
|
|
// Should have node_modules marker in raw data
|
|
expect(content).toContain("[NODE_MODULES:");
|
|
expect(content).toContain("node_modules/lodash");
|
|
});
|
|
});
|