From 93f02325af7ffad50987ab2ba45716c324b9c5a1 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Fri, 24 Jul 2026 19:46:40 -0500 Subject: [PATCH 1/2] fix: refuse to load npm package.json files When Dub finds a package.json with npm-only top-level keys (packageManager, scripts, devDependencies, workspaces, private), raise a hard error instead of parsing it as a Dub recipe. Fixes #3118 Co-authored-by: Cursor --- changelog/refuse-npm-package-json.dd | 13 ++++ source/dub/recipe/io.d | 72 +++++++++++++++++++++ test/issue3118-npm-package-json/.fail_build | 0 test/issue3118-npm-package-json/.no_run | 0 test/issue3118-npm-package-json/.no_test | 0 5 files changed, 85 insertions(+) create mode 100644 changelog/refuse-npm-package-json.dd create mode 100644 test/issue3118-npm-package-json/.fail_build create mode 100644 test/issue3118-npm-package-json/.no_run create mode 100644 test/issue3118-npm-package-json/.no_test diff --git a/changelog/refuse-npm-package-json.dd b/changelog/refuse-npm-package-json.dd new file mode 100644 index 0000000000..107a9befa2 --- /dev/null +++ b/changelog/refuse-npm-package-json.dd @@ -0,0 +1,13 @@ +Refuse to load npm `package.json` files. + +For historic reasons Dub treated `package.json` as an alias for `dub.json`. +When a Node/npm `package.json` was present and no `dub.json`/`dub.sdl` was +found, Dub parsed it anyway, emitted confusing unknown-key warnings, and often +failed with an empty-library / `targetType: none` error. + +Dub now refuses to load a file named `package.json` when it contains npm-only +top-level keys (`packageManager`, `scripts`, `devDependencies`, `workspaces`, +or `private`). Legacy Dub recipes that still use the `package.json` filename +continue to work. Prefer `dub.json` or `dub.sdl` for new packages. + +$(LINK2 https://github.com/dlang/dub/issues/3118, #3118) diff --git a/source/dub/recipe/io.d b/source/dub/recipe/io.d index 4cc7244971..d0319d6e84 100644 --- a/source/dub/recipe/io.d +++ b/source/dub/recipe/io.d @@ -82,6 +82,7 @@ PackageRecipe parsePackageRecipe(string contents, string filename, string default_package_name = null, StrictMode mode = StrictMode.Ignore) { import std.algorithm : endsWith; + import std.path : baseName; import dub.compilers.buildsettings : TargetType; import dub.recipe.sdl : parseSDL; @@ -91,6 +92,11 @@ PackageRecipe parsePackageRecipe(string contents, string filename, if (filename.endsWith(".json")) { + // Historic alias for dub.json; refuse npm's package.json to avoid + // confusing parse warnings / empty-library errors (see #3118). + if (baseName(filename) == "package.json") + enforceNotNpmPackageJson(contents, filename); + ret = parseConfigString!PackageRecipe(contents, filename, mode); fixDependenciesNames(ret.name, ret); } @@ -115,6 +121,72 @@ PackageRecipe parsePackageRecipe(string contents, string filename, return ret; } +/** Top-level npm keys that Dub recipes never use. + + Presence of any of these in a file named `package.json` means the file + belongs to npm (or a similar JS package manager), not Dub. +*/ +private static immutable string[] npmPackageJsonSentinelKeys = [ + "packageManager", + "scripts", + "devDependencies", + "workspaces", + "private", +]; + +/// Throws if `contents` looks like an npm `package.json` rather than a Dub recipe. +private void enforceNotNpmPackageJson(string contents, string filename) +{ + import std.exception : enforce; + import std.format : format; + import dub.internal.vibecompat.data.json : Json, parseJsonString; + + Json json; + try + json = parseJsonString(contents, filename); + catch (Exception) + return; // Let the normal recipe parser report JSON syntax errors. + + if (json.type != Json.Type.object) + return; + + string[] found; + foreach (key; npmPackageJsonSentinelKeys) + if (key in json) + found ~= key; + + enforce(found.length == 0, + format("%s: Refusing to load npm package.json (found non-Dub key%s: %-(%s, %)). " ~ + "Use dub.json or dub.sdl for Dub packages.", + filename, found.length == 1 ? "" : "s", found)); +} + +unittest { // issue #3118 - refuse npm package.json, keep legacy Dub package.json + import std.exception : assertThrown, assertNotThrown; + + assertThrown!Exception(parsePackageRecipe( + `{ "name": "web", "private": true, "devDependencies": { "antora": "^3.1.14" }, "packageManager": "pnpm@9.15.0" }`, + "package.json")); + + assertThrown!Exception(parsePackageRecipe( + `{ "name": "web", "scripts": { "build": "antora antora-playbook.yml" } }`, + "package.json")); + + assertThrown!Exception(parsePackageRecipe( + `{ "name": "web", "workspaces": ["packages/*"] }`, + "package.json")); + + // Legacy Dub recipes named package.json remain valid. + assertNotThrown(parsePackageRecipe( + `{ "name": "exec-simple", "targetType": "executable" }`, + "package.json")); + + // Same npm-looking content is fine under dub.json (unknown keys handled by StrictMode). + assertNotThrown(parsePackageRecipe( + `{ "name": "web", "private": true }`, + "dub.json")); +} + unittest { // issue #711 - configuration default target type not correct for SDL import dub.compilers.buildsettings : TargetType; diff --git a/test/issue3118-npm-package-json/.fail_build b/test/issue3118-npm-package-json/.fail_build new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/issue3118-npm-package-json/.no_run b/test/issue3118-npm-package-json/.no_run new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/issue3118-npm-package-json/.no_test b/test/issue3118-npm-package-json/.no_test new file mode 100644 index 0000000000..e69de29bb2 From 24a938338c56c9d5085a2f5c6d43286747b7e722 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Fri, 24 Jul 2026 19:50:02 -0500 Subject: [PATCH 2/2] test: include npm package.json fixture for #3118 Force-track package.json under test/ (gitignore only allowlists dub.json) and allowlist package.json test fixtures going forward. Co-authored-by: Cursor --- .gitignore | 1 + test/issue3118-npm-package-json/package.json | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 test/issue3118-npm-package-json/package.json diff --git a/.gitignore b/.gitignore index 3240ed7ade..60dbc42fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ __dummy.html !/test/*/.min_frontend !/test/*/.fail_build !/test/*/dub.json +!/test/*/package.json !/test/*/dub.sdl !/test/*/dub.settings.json !/test/*/source/ diff --git a/test/issue3118-npm-package-json/package.json b/test/issue3118-npm-package-json/package.json new file mode 100644 index 0000000000..9fe4a09bda --- /dev/null +++ b/test/issue3118-npm-package-json/package.json @@ -0,0 +1,8 @@ +{ + "name": "npm-looking-package", + "private": true, + "devDependencies": { + "antora": "^3.1.14" + }, + "packageManager": "pnpm@9.15.0" +}