Skip to content

Fix ENOENT crash on clean builds with merge: true (#930)#931

Open
justin808 wants to merge 4 commits intomainfrom
jg/930-fix-sass-resolve
Open

Fix ENOENT crash on clean builds with merge: true (#930)#931
justin808 wants to merge 4 commits intomainfrom
jg/930-fix-sass-resolve

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Feb 27, 2026

Summary

  • Fixes WebpackAssetsManifest ENOENT crash when merge: true and no manifest file exists (clean builds, Docker, CI, after clobber)
  • Seeds an empty {} manifest file (and creates parent directories) before instantiating the plugin
  • Affects webpack-assets-manifest v6 which lacks a catch block in maybeMerge()

Details

Since v9.3.0, shakapacker configures WebpackAssetsManifest with merge: true. On a clean build where public/packs/manifest.json doesn't exist yet, webpack-assets-manifest v6's maybeMerge() throws an unhandled ENOENT because it has try/finally with no catch. v5 has an empty catch that swallows the error, but v6 removed it.

The fix ensures the manifest file exists before the plugin is instantiated, so the merge read succeeds with {} on clean builds.

Fixes #930

Test plan

  • All 349 JS tests pass
  • Lint passes
  • Verify on a clean build with webpack-assets-manifest v6 (Docker/CI)
  • Verify existing manifest content is preserved on subsequent builds

🤖 Generated with Claude Code


Note

Low Risk
Low risk change limited to build-time behavior: it pre-creates the webpack manifest file/directories to avoid an ENOENT crash when webpack-assets-manifest runs with merge: true.

Overview
Prevents clean builds from crashing when webpack-assets-manifest is configured with merge: true but the manifest file doesn’t exist yet.

webpack.ts now ensures config.manifestPath (and its parent directory) exists by seeding an empty {} file before instantiating WebpackAssetsManifest.

Written by Cursor Bugbot for commit 27099f5. Configure here.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed an ENOENT crash that occurred during webpack builds with webpack-assets-manifest v6 when merge mode is enabled.
  • Tests

    • Added test coverage for manifest file initialization and directory creation.

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4636955 and 9fab462.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • package/plugins/webpack.ts
  • package/utils/ensureManifestExists.ts
  • test/package/utils/ensureManifestExists.test.js

Walkthrough

The changes address issue #930 by pre-creating an empty manifest file ({}) in the build output directory before webpack executes. This prevents the ENOENT crash when webpack-assets-manifest v6 attempts to merge assets on clean builds where no manifest exists. A new utility module handles directory creation and safe file seeding.

Changes

Cohort / File(s) Summary
Webpack Plugin Integration
package/plugins/webpack.ts
Added call to ensureManifestExists(config.manifestPath) at the start of getPlugins() to initialize the manifest file before webpack-assets-manifest plugin configuration.
Manifest Initialization Utility
package/utils/ensureManifestExists.ts
New utility module that creates parent directories recursively and seeds an empty {} JSON file at the specified manifest path using the wx flag, with error handling to skip if file already exists.
Utility Tests
test/package/utils/ensureManifestExists.test.js
Comprehensive test suite covering file creation on clean builds, preservation of existing manifests, and recursive parent directory creation with temporary directory cleanup.
Documentation
CHANGELOG.md
Changelog entry documenting the fix for ENOENT crashes on clean builds with webpack-assets-manifest v6 and merge: true.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A manifest fair, where none did dwell,
Now blooms as JSON, a hopeful shell!
Before the webpack dance, we seed the way,
No ENOENT crashes shall ruin the day! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly addresses the main issue: fixing an ENOENT crash on clean builds when merge: true is configured, which is the core objective of the changes.
Linked Issues check ✅ Passed The pull request successfully implements the fix for issue #930 by seeding an empty manifest file before plugin instantiation, preventing the ENOENT crash on clean builds with merge: true.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the ENOENT crash: new utility function, webpack plugin integration, changelog entry, and comprehensive tests—no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jg/930-fix-sass-resolve

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Feb 27, 2026

Review of PR #931 – Fix ENOENT crash on clean builds with merge: true

The fix is clearly necessary and the approach (seeding an empty manifest before the plugin instantiates) is the right one. A few things worth discussing before merging:


1. Import style inconsistency

The file already uses ES module import syntax for its first two lines:

import type { Config } from "../types"
import { getFilteredEnv } from "./envFilter"

But the new additions use CommonJS require():

const fs = require("fs")
const path = require("path")

Every other file in the project that uses fs imports it with ES module syntax (e.g. import { existsSync, mkdirSync, writeFileSync } from "fs" in configExporter/fileWriter.ts). For consistency the new imports should follow the same pattern.


2. No tests for the new function

The PR description mentions "All 349 JS tests pass" but there is no new test covering ensureManifestExists. The existing plugin test directory (test/package/plugins/) only has envFiltering.test.js. A test that verifies:

  • The manifest file is created (with {} content) when it does not exist
  • The manifest file is left untouched when it already exists
  • Parent directories are created if they are missing

…would protect against regressions and document the intended behavior.


3. TOCTOU race condition (minor / low severity)

The existsSync → writeFileSync pattern has a time-of-check/time-of-use window. In a typical single-process webpack build this is harmless, but if two webpack processes start simultaneously against the same output directory (e.g. parallel CI shards writing to the same public path), both could pass the existsSync check and one's `{}" seed could overwrite the other's real manifest.

A simple mitigation is to wrap in a try/catch and ignore EEXIST:

const ensureManifestExists = (manifestPath: string): void => {
  if (!existsSync(manifestPath)) {
    mkdirSync(dirname(manifestPath), { recursive: true })
    try {
      writeFileSync(manifestPath, "{}", { flag: "wx" })  // wx = exclusive create, fails if exists
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err
    }
  }
}

Using the "wx" flag makes the write atomic (on POSIX) and silently handles the race.


4. Side-effect placement inside getPlugins()

ensureManifestExists is called in the middle of getPlugins(), between the constructor resolution block and the plugins array. Moving it to the very top of getPlugins() (before any conditional logic) would make the side effect easier to spot and reason about.


Overall the fix is correct and low-risk. Items 1 and 4 are style/cleanliness nits; item 2 (tests) would meaningfully improve maintainability; item 3 is a minor robustness improvement.

Comment on lines +4 to +5
const fs = require("fs")
const path = require("path")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines use CommonJS require(), but every other file in the project that imports from "fs" uses ES module syntax (e.g. import { existsSync, mkdirSync, writeFileSync } from "fs"). The file already mixes both styles (the first two lines are ES import statements), but adding more require() calls deepens that inconsistency.

Suggested change
const fs = require("fs")
const path = require("path")
import { existsSync, mkdirSync, writeFileSync } from "fs"
import { dirname } from "path"

Then update ensureManifestExists to use existsSync, mkdirSync, writeFileSync, and dirname directly.

Comment on lines +14 to +19
const ensureManifestExists = (manifestPath: string): void => {
if (!fs.existsSync(manifestPath)) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
fs.writeFileSync(manifestPath, "{}")
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existsSync → writeFileSync sequence has a small TOCTOU window: two webpack processes starting simultaneously could both pass the existsSync check and one could overwrite a real manifest written by the other. Using the "wx" (exclusive-create) flag makes the write atomic on POSIX and turns the race into a silent no-op:

Suggested change
const ensureManifestExists = (manifestPath: string): void => {
if (!fs.existsSync(manifestPath)) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
fs.writeFileSync(manifestPath, "{}")
}
}
const ensureManifestExists = (manifestPath: string): void => {
if (!fs.existsSync(manifestPath)) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
try {
fs.writeFileSync(manifestPath, "{}", { flag: "wx" })
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err
}
}
}

Comment on lines +21 to +30
const getPlugins = (): unknown[] => {
// TODO: Remove WebpackAssetsManifestConstructor workaround when dropping 'webpack-assets-manifest < 6.0.0' (Node >=20.10.0) support
const WebpackAssetsManifestConstructor =
"WebpackAssetsManifest" in WebpackAssetsManifest
? WebpackAssetsManifest.WebpackAssetsManifest
: WebpackAssetsManifest

// webpack-assets-manifest v6 crashes with ENOENT when merge: true
// and the manifest file doesn't exist yet (clean builds, Docker, CI).
ensureManifestExists(config.manifestPath)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call sits in the middle of getPlugins(), sandwiched between the constructor workaround and the plugins array. As a file-system side effect it's easy to miss here. Consider moving it to the very top of getPlugins() so the intent is immediately visible:

Suggested change
const getPlugins = (): unknown[] => {
// TODO: Remove WebpackAssetsManifestConstructor workaround when dropping 'webpack-assets-manifest < 6.0.0' (Node >=20.10.0) support
const WebpackAssetsManifestConstructor =
"WebpackAssetsManifest" in WebpackAssetsManifest
? WebpackAssetsManifest.WebpackAssetsManifest
: WebpackAssetsManifest
// webpack-assets-manifest v6 crashes with ENOENT when merge: true
// and the manifest file doesn't exist yet (clean builds, Docker, CI).
ensureManifestExists(config.manifestPath)
const getPlugins = (): unknown[] => {
// webpack-assets-manifest v6 crashes with ENOENT when merge: true
// and the manifest file doesn't exist yet (clean builds, Docker, CI).
ensureManifestExists(config.manifestPath)
// TODO: Remove WebpackAssetsManifestConstructor workaround when dropping 'webpack-assets-manifest < 6.0.0' (Node >=20.10.0) support
const WebpackAssetsManifestConstructor =
"WebpackAssetsManifest" in WebpackAssetsManifest
? WebpackAssetsManifest.WebpackAssetsManifest
: WebpackAssetsManifest

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

This PR fixes an ENOENT crash that occurs during clean builds when webpack-assets-manifest v6 is configured with merge: true. The root cause is that v6 removed the empty catch block from maybeMerge() that v5 had, so when the manifest file doesn't exist, the error propagates instead of being silently swallowed.

The fix adds an ensureManifestExists() helper function that seeds an empty {} manifest file (creating parent directories as needed) before WebpackAssetsManifest is instantiated. This ensures the merge operation reads from an existing file on clean builds (Docker, CI, after clobber).

Key changes:

  • Added fs and path imports using require() (consistent with file style)
  • Created ensureManifestExists() helper with directory creation and empty manifest seeding
  • Called helper before plugin instantiation in getPlugins()
  • Added explanatory comment about v6 ENOENT issue

The fix is minimal, webpack-specific (rspack uses a different plugin that doesn't have this issue), and preserves existing manifest content on subsequent builds since the file check prevents overwriting.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The fix is minimal, well-targeted, and addresses a specific crash scenario. The implementation creates an empty manifest file only when it doesn't exist, avoiding any interference with existing builds. The synchronous file operations are appropriate for webpack configuration phase, and any permission/IO errors will fail fast as expected. All 349 JS tests pass.
  • No files require special attention

Important Files Changed

Filename Overview
package/plugins/webpack.ts Adds ensureManifestExists() helper to seed empty manifest before WebpackAssetsManifest instantiation, preventing ENOENT crashes on clean builds

Last reviewed commit: 4636955

Comment on lines +4 to +5
const fs = require("fs")
const path = require("path")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines use CommonJS require() while the file's existing imports at the top use ES module syntax (import type …, import { … }). All other files in the project that use fs (e.g. package/configExporter/fileWriter.ts) follow the ES module convention. Please align with the rest of the file:

Suggested change
const fs = require("fs")
const path = require("path")
import { existsSync, mkdirSync, writeFileSync } from "fs"
import { dirname } from "path"

Comment on lines +14 to +19
const ensureManifestExists = (manifestPath: string): void => {
if (!fs.existsSync(manifestPath)) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
fs.writeFileSync(manifestPath, "{}")
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here:

1. TOCTOU race condition – the existsSync → writeFileSync sequence has a check-then-act window. Two simultaneous webpack processes (e.g. parallel CI shards sharing the same public directory) could both pass existsSync, and whichever writes second silently overwrites the other's in-progress manifest. Using the "wx" (exclusive-create) flag makes the write atomic on POSIX:

Suggested change
const ensureManifestExists = (manifestPath: string): void => {
if (!fs.existsSync(manifestPath)) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
fs.writeFileSync(manifestPath, "{}")
}
}
const ensureManifestExists = (manifestPath: string): void => {
if (!fs.existsSync(manifestPath)) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
try {
fs.writeFileSync(manifestPath, "{}", { flag: "wx" })
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err
}
}
}

2. Missing tests – there is no test covering the new function. A test that verifies (a) the file is created with {} when absent, (b) an existing file is left untouched, and (c) missing parent directories are created would protect against regressions and document intent. test/package/plugins/ would be the natural home.


// webpack-assets-manifest v6 crashes with ENOENT when merge: true
// and the manifest file doesn't exist yet (clean builds, Docker, CI).
ensureManifestExists(config.manifestPath)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor placement nit: this side-effecting call sits in the middle of getPlugins(), between the constructor-resolution block and the plugin array. Moving it to the very top of the function (before any conditional logic) makes the side effect easier to spot when reading through the code.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
package/plugins/webpack.ts (1)

14-18: Use atomic file creation to avoid TOCTOU edge cases.

Line 15-17 can still race across concurrent processes (existsSync + write). Prefer atomic create (flag: "wx") and ignore EEXIST so an existing manifest is never truncated.

Suggested patch
 const ensureManifestExists = (manifestPath: string): void => {
-  if (!fs.existsSync(manifestPath)) {
-    fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
-    fs.writeFileSync(manifestPath, "{}")
-  }
+  fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
+  try {
+    fs.writeFileSync(manifestPath, "{}", { flag: "wx" })
+  } catch (error) {
+    if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/plugins/webpack.ts` around lines 14 - 18, The ensureManifestExists
function currently uses existsSync + writeFileSync which can race; replace the
non-atomic path with an atomic create: ensure the directory exists (keep
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })) then attempt to
create the file atomically using a write/open with flag "wx" and handle the
EEXIST error by ignoring it (so an existing manifest is not truncated) while
propagating other errors; reference ensureManifestExists and the manifestPath
handling when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@package/plugins/webpack.ts`:
- Around line 14-18: The ensureManifestExists function currently uses existsSync
+ writeFileSync which can race; replace the non-atomic path with an atomic
create: ensure the directory exists (keep
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })) then attempt to
create the file atomically using a write/open with flag "wx" and handle the
EEXIST error by ignoring it (so an existing manifest is not truncated) while
propagating other errors; reference ensureManifestExists and the manifestPath
handling when making this change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2c6e2 and 4636955.

📒 Files selected for processing (2)
  • docs/precompile_hook.md
  • package/plugins/webpack.ts

@claude
Copy link

claude bot commented Feb 27, 2026

Review

The fix correctly addresses the webpack-assets-manifest v6 ENOENT crash on clean builds. The approach of pre-seeding an empty manifest file is sound, and the use of the wx (exclusive write) flag properly handles TOCTOU race conditions between the existsSync check and the writeFileSync call.

A few concerns worth addressing:

Test quality: duplicated logic

The test file reimplements ensureManifestExists locally rather than importing and calling the real function. The comment in the test acknowledges this: since the function is not exported. This validates the algorithm. The problem is that if webpack.ts's implementation drifts from the copy in the test, every test will pass while the production code is broken.

The straightforward fix is to export ensureManifestExists from webpack.ts (or move it to a shared utility module) and import it in the test. That removes the duplicated logic and ensures the tests actually exercise production code.

Brittle source-inspection tests

Two of the five tests read webpack.ts as a text file and assert that specific string literals appear in it — one checks for the wx flag option and another checks for the exact import statements. These break on any refactoring (e.g. autoformatting imports) even when behavior is unchanged, and they add no confidence in runtime behavior. They should be dropped or replaced with behavioral assertions.

rspack path not covered

ensureManifestExists is only called in webpack.ts. rspack.ts uses rspack-manifest-plugin without merge: true, so it likely is not affected by the same bug — but a short comment noting that would save a future reader from having to investigate.

if (err.code !== "EEXIST") throw err
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a copy of the production logic rather than a call to the real function. If webpack.ts is changed but this copy isn't updated, all tests will pass while the live code is broken.

Consider exporting ensureManifestExists from webpack.ts (or extracting it to a small utility file) so this test file can import and exercise the real implementation directly.

)

expect(webpackSource).toContain('{ flag: "wx" }')
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test reads the source file as a text string and checks for a specific literal. It will fail on cosmetic refactors (e.g. reordering options, adding a space) even when behaviour is unchanged, and it doesn't verify anything about runtime correctness.

A behavioral alternative: create a file, delete it, then call ensureManifestExists twice concurrently and assert both calls complete without error and the file contains {}.

expect(webpackSource).toContain('import { dirname } from "path"')
expect(webpackSource).not.toContain('require("fs")')
expect(webpackSource).not.toContain('require("path")')
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as the previous source-inspection test — this asserts on import syntax rather than behavior. If the imports are reformatted or the function is moved to a utility module that uses require, this will fail despite the logic being identical. These two source-inspection tests can safely be dropped once the function is exported and tested directly.

@@ -0,0 +1,51 @@
const fs = require("fs")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file is placed under test/package/plugins/ but the source it tests lives at package/utils/ensureManifestExists.ts. To keep the test tree mirroring the source tree, this should be moved to test/package/utils/ensureManifestExists.test.js.

The existing neighbour test/package/plugins/envFiltering.test.js tests package/plugins/envFilter.ts, so that directory convention is intentional. This file just ended up in the wrong sub-folder.

// exist yet. rspack.ts uses rspack-manifest-plugin without merge: true,
// so it is not affected.
const ensureManifestExists = (manifestPath: string): void => {
if (!existsSync(manifestPath)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The outer existsSync check is redundant and introduces a classic TOCTOU race window: between the existsSync call returning false and the subsequent writeFileSync, another concurrent build process could create the file.

The two inner operations are already safe without the guard:

  • mkdirSync(…, { recursive: true }) is idempotent — it does not throw when the directory already exists.
  • writeFileSync(…, { flag: "wx" }) + EEXIST-catch is idempotent — it skips writing if the file already exists.

Simplified version:

const ensureManifestExists = (manifestPath: string): void => {
  mkdirSync(dirname(manifestPath), { recursive: true })
  try {
    writeFileSync(manifestPath, "{}", { flag: "wx" })
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err
  }
}

This is strictly more correct (no TOCTOU) and slightly simpler. The performance difference of one extra syscall per build is immeasurable.

@claude
Copy link

claude bot commented Feb 27, 2026

Review

The fix is well-motivated and the approach is sound. The root cause analysis (v6 of webpack-assets-manifest dropped the catch block in maybeMerge()) is correctly identified, and pre-seeding an empty {} file is the right workaround.

A few things to address before merging:

Missing CHANGELOG entry

This is a user-visible bug fix (ENOENT crash on clean builds / after assets:clobber / in Docker CI) and belongs under ### Fixed in the [Unreleased] section, e.g.:

- **Fixed ENOENT crash on clean builds when using `webpack-assets-manifest` v6 with `merge: true`**. [PR #931](https://github.com/shakacode/shakapacker/pull/931) by [justin808](https://github.com/justin808). Seeds an empty `{}` manifest file (and its parent directories) before instantiating the plugin, so the merge read succeeds on first build rather than throwing an unhandled ENOENT.

Structural / code notes (see inline comments)

  1. Test file locationtest/package/plugins/ensureManifestExists.test.js should be test/package/utils/ensureManifestExists.test.js to mirror the source tree (package/utils/ensureManifestExists.ts).

  2. Redundant existsSync guard — the outer if (!existsSync(manifestPath)) introduces a TOCTOU race window and is unnecessary because mkdirSync({recursive:true}) and writeFileSync({flag:"wx"}) + EEXIST catch are both already idempotent. Removing it simplifies the code and eliminates the race.

Everything else looks good

  • The { flag: "wx" } + EEXIST catch pattern is the correct Node.js idiom for atomic "create-only" writes.
  • Unit tests cover the three key cases (missing file, existing file preservation, nested directories).
  • The comment explaining why rspack is unaffected is a nice touch.
  • The doc table re-formatting is a clean improvement.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c69987f4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const { moduleExists } = require("../utils/helpers")

const getPlugins = (): unknown[] => {
ensureManifestExists(config.manifestPath)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Defer manifest seeding until compilation succeeds

Calling ensureManifestExists(config.manifestPath) before compilation means a failed first compile still leaves a brand-new manifest file on disk, which makes MtimeStrategy#fresh? treat assets as up to date (manifest_mtime > latest_modified_timestamp) until a watched file changes. In the default development mtime flow, this can stop automatic recompilation after non-source-related failures (for example, transient tooling/environment errors), leaving an empty {} manifest and repeated missing-entry errors instead of retrying compilation on subsequent requests.

Useful? React with 👍 / 👎.

justin808 and others added 4 commits February 28, 2026 10:30
webpack-assets-manifest v6 crashes with ENOENT when merge: true and
the manifest file doesn't exist yet (clean builds, Docker, CI).
Ensure the manifest file and its parent directory exist before
instantiating the plugin.

Fixes #930

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Use ES module imports for fs and path instead of require()
- Fix TOCTOU race condition with wx exclusive-create flag
- Move ensureManifestExists call to top of getPlugins()
- Add tests for manifest creation, preservation, and directory handling

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Move ensureManifestExists from webpack.ts to package/utils/ensureManifestExists.ts
  so it can be exported and tested directly
- Update test to import the real function instead of reimplementing it
- Remove source-inspection tests (wx flag check, ES import check) that break
  on cosmetic refactors without testing runtime behavior
- Add comment noting rspack is unaffected (no merge: true)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add CHANGELOG entry for ENOENT crash fix with webpack-assets-manifest v6
- Move test file from test/package/plugins/ to test/package/utils/ to
  mirror source tree structure
- Remove redundant existsSync guard in ensureManifestExists to eliminate
  TOCTOU race window; mkdirSync and writeFileSync({flag:"wx"}) are
  already idempotent

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@justin808 justin808 force-pushed the jg/930-fix-sass-resolve branch from 9c69987 to 9fab462 Compare February 28, 2026 20:31
@claude
Copy link

claude bot commented Feb 28, 2026

Review

This is a clean, minimal fix for a real bug. The implementation is correct and well-tested.

What the fix does

Seeds an empty {} manifest file before WebpackAssetsManifest is instantiated, so that maybeMerge() in v6 (which lost its empty catch) doesn't crash on clean builds where the file doesn't yet exist.

Strengths

  • Correct TOCTOU handling: Uses writeFileSync with flag: "wx" (exclusive create) rather than a check-then-write pattern. This atomically handles the race between existence check and write, and idempotently handles the case where the file already exists by catching EEXIST instead of re-checking.
  • Minimal scope: Only touches webpack.ts (not rspack.ts, which uses rspack-manifest-plugin without merge: true), and the comment in the utility file correctly documents this distinction.
  • Test coverage: Tests cover all three meaningful cases — creating a new file, preserving existing content, and creating nested directories.
  • Correct placement: ensureManifestExists is called at the top of getPlugins(), before the plugin constructor, which is exactly the right place.

Minor observations

  1. Unhandled mkdirSync errors: If mkdirSync fails for a reason other than the directory already existing (e.g., permission denied), the error will propagate with a generic filesystem message. This is acceptable behavior — if the directory can't be created, the build should fail — but it might be worth wrapping it similarly if a more user-friendly error message is desired in future.

  2. Synchronous I/O: Using sync fs operations during webpack config generation is fine since webpack config is evaluated synchronously. No concern here.

  3. Watch mode: getPlugins() is called once during config generation, not on each rebuild, so the sync filesystem calls won't add latency to incremental builds.

Verdict

LGTM. The fix is correct, minimal, and well-tested. The wx flag approach is idiomatic and properly handles the concurrent/repeated-call case without a TOCTOU window.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

merge: true on WebpackAssetsManifest causes ENOENT on clean builds (v9.3.0+)

1 participant