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 < @@ -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/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); 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/npm/test/bazel/BUILD.bazel b/npm/test/bazel/BUILD.bazel index 83b7b288..0e0428cc 100644 --- a/npm/test/bazel/BUILD.bazel +++ b/npm/test/bazel/BUILD.bazel @@ -8,4 +8,5 @@ exports_files( yarn_resolve( name = "resolve", output = "npm.bzl", + patches = "npm-patches", ) diff --git a/npm/test/bazel/package.json b/npm/test/bazel/package.json index 8c836b7d..3f200a74 100644 --- a/npm/test/bazel/package.json +++ b/npm/test/bazel/package.json @@ -1,5 +1,5 @@ { "devDependencies": { - "lodash": "^4.17.21" + "lodash": "^4.18.1" } } diff --git a/npm/test/bazel/yarn.lock b/npm/test/bazel/yarn.lock index d65403c0..1848f2ac 100644 --- a/npm/test/bazel/yarn.lock +++ b/npm/test/bazel/yarn.lock @@ -5,10 +5,9 @@ __metadata: version: 8 cacheKey: 10c0 -"lodash@npm:^4.17.21": - version: 4.17.23 - resolution: "lodash@npm:4.17.23" - checksum: 10c0/1264a90469f5bb95d4739c43eb6277d15b6d9e186df4ac68c3620443160fc669e2f14c11e7d8b2ccf078b81d06147c01a8ccced9aab9f9f63d50dcf8cace6bf6 +"lodash@npm:^4.18.1": + version: 4.18.1 + resolution: "lodash@npm:4.18.1" languageName: node linkType: hard @@ -16,6 +15,6 @@ __metadata: version: 0.0.0-use.local resolution: "root-workspace-0b6124@workspace:." dependencies: - lodash: "npm:^4.17.21" + lodash: "npm:^4.18.1" languageName: unknown linkType: soft 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], 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); });