From 7e1ec69ce4af68c2b0e591f30effd556701b1f88 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 6 Jul 2026 10:54:33 -0600 Subject: [PATCH 1/4] Fix Vite cache identity for nodejs_install Write a package-manager state marker from the Bazel install manifest so tools that key dependency caches from node_modules state invalidate when the generated layout changes. Co-Authored-By: OpenAI Codex --- docs/nodejs.md | 22 +++++++++++++++------- nodejs/doc/doc.md | 7 +++++++ nodejs/rules.bzl | 3 ++- pkg/install-runner.sh.tpl | 2 +- pkg/install/src/main.ts | 39 +++++++++++++++++++++++++++++++++++++++ pkg/rules.bzl | 6 ++++++ 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/docs/nodejs.md b/docs/nodejs.md index 8a438f03..e90b6e9b 100644 --- a/docs/nodejs.md +++ b/docs/nodejs.md @@ -111,6 +111,13 @@ Then run: bazel run :nodejs_install ``` +`nodejs_install` writes `node_modules/.install-manifest.json` and, by default, +`node_modules/.yarn-state.yml`. The Yarn state file contains a digest of the +generated install manifest so tools such as Vite, which key dependency caches +from package-manager install state, invalidate caches when the Bazel-generated +`node_modules` layout changes. Set `package_manager_state_file = ""` to disable +this marker. + # //nodejs:nodejs.bzl @@ -400,17 +407,18 @@ nodejs_toolchain(name, name, src, path, **kwargs) +nodejs_install(name, src, path, package_manager_state_file, **kwargs) **PARAMETERS** -| Name | Description | Default Value | -| :--------------------------------------- | :------------------------ | :------------ | -| name |

-

| none | -| src |

-

| none | -| path |

-

| `None` | -| kwargs |

-

| none | +| Name | Description | Default Value | +| :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------ | +| name |

-

| none | +| src |

-

| none | +| path |

-

| `None` | +| package_manager_state_file | Package manager install-state file to write under `node_modules`. The default `.yarn-state.yml` lets tools such as Vite invalidate dependency caches when the generated install manifest changes. Set to `""` to disable. | `".yarn-state.yml"` | +| kwargs |

-

| none | diff --git a/nodejs/doc/doc.md b/nodejs/doc/doc.md index 4c5939b1..431c83c2 100644 --- a/nodejs/doc/doc.md +++ b/nodejs/doc/doc.md @@ -84,3 +84,10 @@ Then run: ```sh bazel run :nodejs_install ``` + +`nodejs_install` writes `node_modules/.install-manifest.json` and, by default, +`node_modules/.yarn-state.yml`. The Yarn state file contains a digest of the +generated install manifest so tools such as Vite, which key dependency caches +from package-manager install state, invalidate caches when the Bazel-generated +`node_modules` layout changes. Set `package_manager_state_file = ""` to disable +this marker. diff --git a/nodejs/rules.bzl b/nodejs/rules.bzl index d9ea6314..c1d4753b 100644 --- a/nodejs/rules.bzl +++ b/nodejs/rules.bzl @@ -630,10 +630,11 @@ nodejs_repl = rule( implementation = _nodejs_repl_impl, ) -def nodejs_install(name, src, path = None, **kwargs): +def nodejs_install(name, src, path = None, package_manager_state_file = ".yarn-state.yml", **kwargs): pkg_install( name = name, pkg = src, + package_manager_state_file = package_manager_state_file, path = "%s/node_modules" % path if path else "node_modules", **kwargs ) diff --git a/pkg/install-runner.sh.tpl b/pkg/install-runner.sh.tpl index 3b6b8917..47abe390 100644 --- a/pkg/install-runner.sh.tpl +++ b/pkg/install-runner.sh.tpl @@ -8,4 +8,4 @@ if [ -z "${RUNFILES_DIR-}" ]; then fi fi -exec "$RUNFILES_DIR"/%{install} "$RUNFILES_DIR"/%{manifest} "${BUILD_WORKSPACE_DIRECTORY-.}"/%{path} +exec "$RUNFILES_DIR"/%{install} %{package_manager_state_args} "$RUNFILES_DIR"/%{manifest} "${BUILD_WORKSPACE_DIRECTORY-.}"/%{path} diff --git a/pkg/install/src/main.ts b/pkg/install/src/main.ts index e6d18fdf..2b563d91 100644 --- a/pkg/install/src/main.ts +++ b/pkg/install/src/main.ts @@ -6,6 +6,7 @@ import { } from "@rules-javascript/pkg-install-manifest"; import { JsonFormat } from "@rules-javascript/util-json"; import { ArgumentParser } from "argparse"; +import { createHash } from "node:crypto"; import { chmod, copyFile, @@ -19,6 +20,7 @@ import { join } from "node:path"; (async () => { const parser = new ArgumentParser(); + parser.add_argument("--package-manager-state-file"); parser.add_argument("manifest"); parser.add_argument("output"); const args = parser.parse_args(); @@ -30,10 +32,14 @@ import { join } from "node:path"; ); const existingPath = join(args.output, ".install-manifest.json"); + const packageManagerStatePath = args.package_manager_state_file + ? join(args.output, args.package_manager_state_file) + : undefined; let existing: InstallManifest | undefined; try { const existingContent = await readFile(existingPath, "utf8"); if (manifestContent === existingContent) { + await writePackageManagerState(packageManagerStatePath, manifestContent); return; } existing = JsonFormat.parse(installManifestFormat(false), existingContent); @@ -48,11 +54,44 @@ import { join } from "node:path"; await install(manifest, existing, args.output); await writeFile(existingPath, manifestContent); + await writePackageManagerState(packageManagerStatePath, manifestContent); })().catch((error) => { console.error(error); process.exit(1); }); +async function writePackageManagerState( + path: string | undefined, + manifestContent: string, +) { + if (!path) { + return; + } + const manifestSha256 = createHash("sha256") + .update(manifestContent) + .digest("hex"); + const content = [ + "# Warning: This file is automatically generated. Removing it is fine, but will", + "# cause your node_modules installation to become invalidated.", + "", + "__metadata:", + " version: 1", + " nmMode: classic", + ` rulesJavascriptInstallManifestSha256: ${manifestSha256}`, + "", + ].join("\n"); + try { + if ((await readFile(path, "utf8")) === content) { + return; + } + } catch (error) { + if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) { + throw error; + } + } + await writeFile(path, content); +} + async function install( manifest: InstallEntry, existing: InstallEntry | undefined, diff --git a/pkg/rules.bzl b/pkg/rules.bzl index db9628cb..02a5e8c7 100644 --- a/pkg/rules.bzl +++ b/pkg/rules.bzl @@ -124,6 +124,9 @@ def _pkg_install_impl(ctx): substitutions = { "%{install}": shell.quote(to_rlocation_path(ctx, install)), "%{manifest}": shell.quote(to_rlocation_path(ctx, manifest)), + "%{package_manager_state_args}": ( + "--package-manager-state-file %s" % shell.quote(ctx.attr.package_manager_state_file) if ctx.attr.package_manager_state_file else "" + ), "%{path}": shell.quote(path), }, is_executable = True, @@ -141,6 +144,9 @@ def _pkg_install_impl(ctx): pkg_install = rule( attrs = { "path": attr.string(mandatory = True), + "package_manager_state_file": attr.string( + doc = "Optional package manager install-state file to write under path, for tools that key caches from package manager state.", + ), "pkg": attr.label( mandatory = True, providers = [PackageFilegroupInfo], From 2747afaf36f8e83b370162df857c5a2d286cd80e Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 6 Jul 2026 11:08:22 -0600 Subject: [PATCH 2/4] Restore configured Rollup compatibility Allow existing callers to keep using configure_rollup while newer rollup_bundle callers pass config directly. Co-Authored-By: OpenAI Codex --- rollup/rules.bzl | 57 ++++++++++++++++++++++++++++- rollup/test/bazel/basic/BUILD.bazel | 15 +++++++- rollup/test/src/basic.spec.ts | 14 ++++--- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/rollup/rules.bzl b/rollup/rules.bzl index 0cdcf42b..5044505a 100644 --- a/rollup/rules.bzl +++ b/rollup/rules.bzl @@ -5,7 +5,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//commonjs:providers.bzl", "CjsInfo", "gen_manifest", "package_path") load("//file:file.bzl", "FileInfo") load("//javascript:providers.bzl", "JsInfo") -load("//javascript:rules.bzl", "js_export") +load("//javascript:rules.bzl", "js_dep_file") load("//nodejs:nodejs.bzl", "NodejsInfo") load("//nodejs:rules.bzl", "nodejs_binary") load("//pnp:providers.bzl", "pnp_gen") @@ -128,7 +128,7 @@ def _rollup_bundle_impl(ctx): return [default_info] -rollup_bundle = rule( +_rollup_bundle = rule( attrs = { "config": attr.label( cfg = _rollup_config_transition, @@ -178,3 +178,56 @@ rollup_bundle = rule( doc = "Rollup bundle", implementation = _rollup_bundle_impl, ) + +def configure_rollup(name, dep, config, config_dep, visibility = None): + """Set up a Rollup tool target with a bundled config. + + This macro preserves the older API where rollup_bundle accepted a configured + rollup target instead of a separate config attribute. + """ + + js_dep_file( + name = "%s.config" % name, + dep = config_dep, + path = config, + visibility = ["//visibility:private"], + ) + + rollup( + name = name, + dep = dep, + visibility = visibility, + ) + +def rollup_bundle(name, dep, config = None, rollup = None, output = "", **kwargs): + """Bundle JavaScript with Rollup. + + Prefer passing config directly. If config is omitted, rollup must point to a + target created by configure_rollup. + """ + + if config == None: + if rollup == None: + fail("rollup_bundle requires config unless rollup points to a target created by configure_rollup") + config = _configured_rollup_config_label(rollup) + + attrs = { + "name": name, + "config": config, + "dep": dep, + "output": output, + } + if rollup != None: + attrs["rollup"] = rollup + attrs.update(kwargs) + _rollup_bundle(**attrs) + +def _configured_rollup_config_label(rollup): + rollup = str(rollup) + if ":" in rollup: + return "%s.config" % rollup + if rollup.startswith("//") or rollup.startswith("@"): + package = rollup.split("//", 1)[1] + name = package.rpartition("/")[2] + return "%s:%s.config" % (rollup, name) + return "%s.config" % rollup diff --git a/rollup/test/bazel/basic/BUILD.bazel b/rollup/test/bazel/basic/BUILD.bazel index 863d795a..001951c0 100644 --- a/rollup/test/bazel/basic/BUILD.bazel +++ b/rollup/test/bazel/basic/BUILD.bazel @@ -1,6 +1,6 @@ load("@rules_javascript//commonjs:rules.bzl", "cjs_root") load("@rules_javascript//javascript:rules.bzl", "js_file", "js_library") -load("@rules_javascript//rollup:rules.bzl", "rollup_bundle") +load("@rules_javascript//rollup:rules.bzl", "configure_rollup", "rollup_bundle") cjs_root( name = "root", @@ -32,3 +32,16 @@ rollup_bundle( config = ":rollup_config", dep = ":lib", ) + +configure_rollup( + name = "legacy_rollup", + config = "rollup.config.js", + config_dep = ":rollup_config", + dep = "@npm//rollup:lib", +) + +rollup_bundle( + name = "legacy_bundle", + dep = ":lib", + rollup = ":legacy_rollup", +) diff --git a/rollup/test/src/basic.spec.ts b/rollup/test/src/basic.spec.ts index 6ff2f487..019472e6 100644 --- a/rollup/test/src/basic.spec.ts +++ b/rollup/test/src/basic.spec.ts @@ -2,10 +2,14 @@ import { spawnOptions } from "@rules-javascript/test"; import * as childProcess from "node:child_process"; test("Basic", () => { - const result = childProcess.spawnSync("bazel", ["build", "basic:bundle"], { - cwd: "rollup/test/bazel", - stdio: "inherit", - ...spawnOptions(), - }); + const result = childProcess.spawnSync( + "bazel", + ["build", "basic:bundle", "basic:legacy_bundle"], + { + cwd: "rollup/test/bazel", + stdio: "inherit", + ...spawnOptions(), + }, + ); expect(result.status).toBe(0); }); From f5bf01d96f1694a4a71788642a03ca06c63d19a2 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 6 Jul 2026 12:10:08 -0600 Subject: [PATCH 3/4] Fix rules_javascript CI test setup Make the workflow write the bazelrc file this repo imports, disable worker sandboxing on GitHub runners, and refresh the npm audit fixture so the test suite can run cleanly. Co-Authored-By: OpenAI Codex --- .github/configure-bazel | 7 ++++++- npm/test/bazel/BUILD.bazel | 1 + npm/test/bazel/package.json | 2 +- npm/test/bazel/yarn.lock | 9 ++++----- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/configure-bazel b/.github/configure-bazel index de153866..49ba0a2d 100755 --- a/.github/configure-bazel +++ b/.github/configure-bazel @@ -1,5 +1,10 @@ #!/usr/bin/env sh set -e -cat > user.bazelrc < .bazelrc.user < Date: Mon, 6 Jul 2026 12:17:44 -0600 Subject: [PATCH 4/4] Give snapshot wrapper more time in CI The nested Bazel snapshot run can exceed Jest's default timeout on GitHub runners even after the underlying test passes. Co-Authored-By: OpenAI Codex --- jest/test/src/snapshot.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest/test/src/snapshot.spec.ts b/jest/test/src/snapshot.spec.ts index bcd53f0a..443e561b 100644 --- a/jest/test/src/snapshot.spec.ts +++ b/jest/test/src/snapshot.spec.ts @@ -24,4 +24,4 @@ test("Snapshot", async () => { await fs.promises.unlink( "jest/test/bazel/snapshot/__snapshots__/example.spec.js.snap", ); -}); +}, 120_000);