diff --git a/src/ast/lib.rs b/src/ast/lib.rs index a7e8f91cb162..2bde2b06d547 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -79,8 +79,8 @@ pub enum ImportKind { // arms inside label()/error_label() below — zero runtime init (PORTING.md §Concurrency: prefer no-lock over OnceLock // when the data is pure const). // -// If these are changed, make sure to update -// - src/js/builtins/codegen/replacements.ts +// If the variants or the labels below change, make sure to update +// - src/codegen/replacements.ts (`enums.ImportKind`, indexed by discriminant) // - packages/bun-types/bun.d.ts impl ImportKind { @@ -94,7 +94,7 @@ impl ImportKind { ImportKind::Dynamic => b"dynamic-import", ImportKind::RequireResolve => b"require-resolve", ImportKind::At => b"import-rule", - ImportKind::AtConditional => b"", + ImportKind::AtConditional => b"import-rule", ImportKind::Url => b"url-token", ImportKind::Composes => b"composes", ImportKind::Internal => b"internal", @@ -112,7 +112,7 @@ impl ImportKind { ImportKind::Dynamic => b"import()", ImportKind::RequireResolve => b"require.resolve()", ImportKind::At => b"@import", - ImportKind::AtConditional => b"", + ImportKind::AtConditional => b"@import", ImportKind::Url => b"url()", ImportKind::Internal => b"", ImportKind::Composes => b"composes", diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index af31730db055..36c8d8488504 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -117,16 +117,20 @@ export const enums = { "md", "xml", ], + // Ids are the `bun_ast::ImportKind` discriminants; labels are `ImportKind::label()`. ImportKind: [ - "entry-point-run", - "entry-point-build", - "import-statement", - "require-call", - "dynamic-import", - "require-resolve", - "import-rule", - "url-token", - "internal", + "entry-point-run", // EntryPointRun + "entry-point-build", // EntryPointBuild + "import-statement", // Stmt + "require-call", // Require + "dynamic-import", // Dynamic + "require-resolve", // RequireResolve + "import-rule", // At + "import-rule", // AtConditional + "url-token", // Url + "composes", // Composes + "html_manifest", // HtmlManifest + "internal", // Internal ], }; diff --git a/src/jsc/ResolveMessage.rs b/src/jsc/ResolveMessage.rs index 594da9ebbbcd..a258e8ebf864 100644 --- a/src/jsc/ResolveMessage.rs +++ b/src/jsc/ResolveMessage.rs @@ -35,27 +35,6 @@ impl Default for ResolveMessage { } } -/// `ImportKind.label()` — the canonical table lives in -/// `bun_ast::ImportKind::label`, but -/// `bun_ast::MetadataResolve.import_kind` is the type-only `bun_ast::ImportKind`. -/// Replicate the table here verbatim. -fn import_kind_label(kind: ImportKind) -> &'static [u8] { - match kind { - ImportKind::EntryPointRun => b"entry-point-run", - ImportKind::EntryPointBuild => b"entry-point-build", - ImportKind::Stmt => b"import-statement", - ImportKind::Require => b"require-call", - ImportKind::Dynamic => b"dynamic-import", - ImportKind::RequireResolve => b"require-resolve", - ImportKind::At => b"import-rule", - ImportKind::AtConditional => b"", - ImportKind::Url => b"url-token", - ImportKind::Composes => b"composes", - ImportKind::Internal => b"internal", - ImportKind::HtmlManifest => b"html_manifest", - } -} - /// Host-agnostic bare-specifier check for Node ESM error shaping. Must not vary by host: /// relative, separator-led, and ASCII-letter drive forms are path-like; everything else is a /// package. Unlike `bun_paths::is_absolute`, the drive byte must be alphabetic. @@ -479,7 +458,7 @@ impl ResolveMessage { pub fn get_import_kind(this: &Self, global: &JSGlobalObject) -> JsResult { Ok(match &this.msg.metadata { bun_ast::Metadata::Resolve(resolve) => { - ZigString::init(import_kind_label(resolve.import_kind)).to_js(global) + ZigString::init(resolve.import_kind.label()).to_js(global) } _ => ZigString::init(b"").to_js(global), }) diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index 19d6444da1b4..2c9accf8e0fe 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -214,6 +214,53 @@ describe("Bun.build", () => { } }); + test("css @import resolve errors report importKind 'import-rule' with and without import conditions", async () => { + using dir = tempDir("bun-build-api-css-import-kind", { + "entry.css": ` + @import "./missing-plain.css"; + @import "./missing-conditional.css" supports(display: grid); + @import "bun:sqlite"; + @import "bun:sqlite" supports(display: grid); + `, + }); + + const build = await Bun.build({ + entrypoints: [join(String(dir), "entry.css")], + target: "browser", + throw: false, + }); + + expect(build.success).toBe(false); + for (const log of build.logs) expect(log).toBeInstanceOf(ResolveMessage); + const logs = (build.logs as ResolveMessage[]).toSorted((a, b) => a.position!.line - b.position!.line); + expect(logs.map(({ specifier, importKind, message }) => ({ specifier, importKind, message }))).toEqual([ + { + specifier: "./missing-plain.css", + importKind: "import-rule", + message: 'Could not resolve: "./missing-plain.css"', + }, + { + specifier: "./missing-conditional.css", + importKind: "import-rule", + message: 'Could not resolve: "./missing-conditional.css"', + }, + { + specifier: "bun:sqlite", + importKind: "import-rule", + message: `Browser build cannot @import Bun builtin: "bun:sqlite". When bundling for Bun, set target to 'bun'`, + }, + { + specifier: "bun:sqlite", + importKind: "import-rule", + message: `Browser build cannot @import Bun builtin: "bun:sqlite". When bundling for Bun, set target to 'bun'`, + }, + ]); + expect(JSON.parse(JSON.stringify(logs[1]))).toMatchObject({ + specifier: "./missing-conditional.css", + importKind: "import-rule", + }); + }); + test("returns output files", async () => { Bun.gc(true); const build = await Bun.build({ diff --git a/test/bundler/bundler_plugin.test.ts b/test/bundler/bundler_plugin.test.ts index e009c304867f..d87514993c60 100644 --- a/test/bundler/bundler_plugin.test.ts +++ b/test/bundler/bundler_plugin.test.ts @@ -544,6 +544,56 @@ describe("bundler", () => { }, }; }); + // Every import kind an onResolve callback can receive. Not reachable from a plugin: entry-point-run + // (runtime only), html_manifest (the record is retagged after resolution) and internal (never created). + itBundled("plugin/ResolveKindForEveryImportKind", () => { + const resolved: [string, string][] = []; + return { + files: { + "entry.js": /* js */ ` + import "./styles.css"; + require("./required.js"); + import("./dynamic.js"); + console.log(require.resolve("./resolved.js")); + `, + "required.js": `module.exports = 1;`, + "dynamic.js": `export const dynamic = 1;`, + "resolved.js": `export const resolved = 1;`, + "styles.css": /* css */ ` + @import "./plain.css"; + @import "./conditional.css" supports(display: grid); + @import "./a.module.css"; + .bg { background: url("./image.png"); } + `, + "plain.css": `.plain { color: blue; }`, + "conditional.css": `.conditional { display: grid; }`, + "a.module.css": `.a { composes: b from "./b.module.css"; padding: 1px; }`, + "b.module.css": `.b { color: green; }`, + "image.png": "not really a png", + }, + outdir: "/out", + plugins(builder) { + builder.onResolve({ filter: /.*/ }, args => { + resolved.push([path.basename(args.path), args.kind]); + return undefined; + }); + }, + onAfterBundle() { + expect(resolved.sort()).toEqual([ + ["a.module.css", "import-rule"], + ["b.module.css", "composes"], + ["conditional.css", "import-rule"], + ["dynamic.js", "dynamic-import"], + ["entry.js", "entry-point-build"], + ["image.png", "url-token"], + ["plain.css", "import-rule"], + ["required.js", "require-call"], + ["resolved.js", "require-resolve"], + ["styles.css", "import-statement"], + ]); + }, + }; + }); itBundled("plugin/ManyFiles", ({ root }) => { const FILES = process.platform === "win32" ? 50 : 200; // windows is slower at this const create = (fn: (i: number) => string) => new Array(FILES).fill(0).map((_, i) => fn(i)); diff --git a/test/bundler/metafile.test.ts b/test/bundler/metafile.test.ts index 3ca6436b214d..63fab555dcbf 100644 --- a/test/bundler/metafile.test.ts +++ b/test/bundler/metafile.test.ts @@ -458,6 +458,40 @@ describe("bundler metafile", () => { expect(outputPaths).toContain(dynamicImport!.path); }); + test("metafile tracks css @import imports as import-rule, with or without import conditions", async () => { + using dir = tempDir("metafile-css-import-rule-test", { + // Reached through a JS entry because a CSS entrypoint currently contributes nothing to metafile.inputs. + "entry.js": `import "./styles.css";`, + "styles.css": ` + @import "./plain.css"; + @import "./conditional.css" supports(display: grid); + @import "./media.css" screen; + @import "./layered.css" layer(base) supports(display: grid) screen; + .foo { color: red; } + `, + "plain.css": `.plain { color: blue; }`, + "conditional.css": `.conditional { display: grid; }`, + "media.css": `.media { color: green; }`, + "layered.css": `.layered { color: black; }`, + }); + + const result = await Bun.build({ + entrypoints: [`${dir}/entry.js`], + metafile: true, + }); + + expect(result.success).toBe(true); + const inputs = (result.metafile as Metafile).inputs; + const stylesKey = Object.keys(inputs).find(path => path.endsWith("styles.css"))!; + expect(stylesKey).toBeDefined(); + expect(inputs[stylesKey].imports.map(({ kind, original }) => ({ kind, original }))).toEqual([ + { kind: "import-rule", original: "./plain.css" }, + { kind: "import-rule", original: "./conditional.css" }, + { kind: "import-rule", original: "./media.css" }, + { kind: "import-rule", original: "./layered.css" }, + ]); + }); + test("metafile includes cssBundle for CSS outputs", async () => { using dir = tempDir("metafile-css-bundle-test", { "entry.js": `import "./styles.css"; console.log("styled");`,