From bbe8fedcebeba1b3379d12a4e559eed851b6dea6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 15:44:11 +0000 Subject: [PATCH 1/5] docs: document resolver hooks and their pipeline workflow Adds a new Hooks section to the README that enumerates the lifecycle hooks on resolver.hooks, every pipeline hook in execution order, the before-/after- stage modifiers, and short examples showing tracing, short-circuiting, and doResolve-based rewrites. --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de2b3f24..726ae409 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,93 @@ Plugins are executed in a pipeline, and register which event they should be exec | `UnsafeCachePlugin` | Caches successful resolves in an in-memory map to speed up repeated requests. Powers `unsafeCache`. | | `UseFilePlugin` | Joins a fixed filename onto the current path (e.g. `index`). Powers `mainFiles`. | +### Hooks + +A resolver exposes two kinds of [`tapable`](https://github.com/webpack/tapable) hooks: + +- **Lifecycle hooks** on `resolver.hooks` — fired by the resolver itself around each `resolve` call. Use these to observe, not to transform the request. +- **Pipeline hooks** — the named steps that plugins tap as `source` and forward to as `target`. Every pipeline hook is an `AsyncSeriesBailHook<[request, resolveContext], request | null>`: return `callback()` to pass on, `callback(err)` to fail, or `callback(null, request)` to short-circuit with a result. Obtain them with `resolver.ensureHook(name)` (creates if missing) or `resolver.getHook(name)` (throws if missing); names are kebab-case or camelCase and are interchangeable. + +#### Lifecycle hooks + +| Hook | Type | Fires when | +| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `resolveStep` | `SyncHook` | Every time the resolver hands a request to a pipeline hook. Arguments: `(hook, request)`. Ideal for tracing. | +| `noResolve` | `SyncHook` | When a top-level `resolve()` call can't produce a result. Arguments: `(request, error)`. | +| `resolve` | `AsyncSeriesBailHook`| Entry point of the pipeline (also listed below). Tap this to intercept requests before parsing. | +| `result` | `AsyncSeriesHook` | After a successful resolve, with the final request. Fired by `ResultPlugin`. Tap to observe/record results without short-circuiting. | + +#### Pipeline hooks + +Listed roughly in the order the default pipeline visits them. Full wiring lives in `lib/ResolverFactory.js` under `//// pipeline ////`. + +| Hook | Role | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `resolve` | Entry point. `ParsePlugin` parses the raw request (path, query, fragment, module flag) and forwards to `parsed-resolve`. | +| `internal-resolve` | Re-entry point used by internal rewrites (e.g. after an `alias` fires). Same role as `resolve`, but `fullySpecified` is forced off. | +| `imports-resolve` | Re-entry point for the target of an `imports` field match; prevents recursive `#` resolution per the Node.js ESM spec. | +| `parsed-resolve` | Request has been parsed. `DescriptionFilePlugin` attaches the nearest `package.json`, then forwards to `described-resolve`. | +| `described-resolve` | Description file is attached. Where `unsafeCache`, `fallback`, and most user plugins (including `MyLibSrcPlugin` below) hook in. | +| `raw-resolve` | After description. Where `alias`, `aliasFields`, `tsconfig` paths, and `extensionAlias` rewrites fire before default resolution. | +| `normal-resolve` | Default resolution starts. Branches into `relative` (for `./`, `../`, absolute), `raw-module` (bare modules), or `internal` (`#imports`). | +| `internal` | `#name` imports-field entry. `ImportsFieldPlugin` maps the specifier and forwards to `relative` or `imports-resolve`. | +| `raw-module` | Bare-module lookup. `SelfReferencePlugin`, `ModulesInHierarchicalDirectoriesPlugin`, `ModulesInRootPlugin`, and `PnpPlugin` all tap here. | +| `alternate-raw-module` | Fallback module lookup used by `PnpPlugin` when PnP can't resolve and `node_modules` should be tried. | +| `module` | A candidate module directory was built. `JoinRequestPartPlugin` splits off the inner request and forwards to `resolve-as-module`. | +| `resolve-as-module` | Treat candidate as a package. `DirectoryExistsPlugin` gates on existence; short single-file modules may re-enter via `undescribed-raw-file`. | +| `undescribed-resolve-in-package` | Inside a located package directory, before its `package.json` has been read. Loads the description, forwards to `resolve-in-package`. | +| `resolve-in-package` | Inside a package with its description loaded. `ExportsFieldPlugin` matches `exports`, otherwise forwards to `resolve-in-existing-directory`. | +| `resolve-in-existing-directory` | Package directory confirmed; join the remaining request onto it and continue at `relative`. | +| `relative` | A concrete path on disk. `DescriptionFilePlugin` loads the nearest `package.json` and forwards to `described-relative`. | +| `described-relative` | Branches to `raw-file` (treat as file) and `directory` (treat as directory). `resolveToContext` skips the file branch. | +| `directory` | Candidate directory. `DirectoryExistsPlugin` gates on existence and forwards to `undescribed-existing-directory`. | +| `undescribed-existing-directory` | Existing directory, before its `package.json` has been read. `UseFilePlugin` tries `mainFiles` via `undescribed-raw-file`. | +| `existing-directory` | Existing directory with description loaded. `MainFieldPlugin` tries `mainFields`; `UseFilePlugin` falls back to `mainFiles`. | +| `undescribed-raw-file` | Candidate file path, before description is read. Loads description, then forwards to `raw-file`. | +| `raw-file` | Apply extension handling: `ConditionalPlugin` short-circuits when `fullySpecified`, `TryNextPlugin` + `AppendPlugin` try each extension. | +| `file` | A specific file path. Last place `alias` and `aliasFields` can redirect; forwards to `final-file`. | +| `final-file` | `FileExistsPlugin` checks the file is real and records it as a file dependency, then forwards to `existing-file`. | +| `existing-file` | Real file on disk. `SymlinkPlugin` real-paths symlinks (unless `symlinks: false`), then forwards to `resolved`. | +| `resolved` | Terminal hook. `RestrictionsPlugin` enforces `restrictions`; `ResultPlugin` fires the `result` lifecycle hook. | + +#### `before-` and `after-` prefixes + +`ensureHook("before-foo")` and `getHook("before-foo")` return the `foo` hook with `stage: -10`; `after-foo` returns it with `stage: 10`. Use this to tap earlier or later than the default stage without creating a separate hook. You'll see `after-parsed-resolve`, `after-normal-resolve`, `after-relative`, and `after-undescribed-resolve-in-package` used this way inside `ResolverFactory`. + +#### Hook examples + +Trace every pipeline step and observe failures via the lifecycle hooks: + +```js +resolver.hooks.resolveStep.tap("Trace", (hook, request) => { + console.log(`[step] ${hook.name}: ${request.request} @ ${request.path}`); +}); +resolver.hooks.noResolve.tap("Trace", (request, error) => { + console.log(`[fail] ${request.request}: ${error.message}`); +}); +resolver.hooks.result.tapAsync("Trace", (request, _ctx, callback) => { + console.log(`[done] ${request.path}`); + callback(); +}); +``` + +Short-circuit at `file` to redirect any `.css` request to a stub without continuing the pipeline: + +```js +class StubCssPlugin { + apply(resolver) { + resolver + .getHook("file") + .tapAsync("StubCssPlugin", (request, _ctx, callback) => { + if (!request.path || !request.path.endsWith(".css")) return callback(); + callback(null, { ...request, path: require.resolve("./empty.js") }); + }); + } +} +``` + +Forward to a different hook with `doResolve` to restart resolution with a rewritten request — see `MyLibSrcPlugin` in [Writing a Custom Plugin](#writing-a-custom-plugin) for the canonical pattern (`getHook("described-resolve")` → `doResolve(ensureHook("resolve"), …)`). + ### Writing a Custom Plugin The example below adds a plugin that rewrites any request starting with `my-lib/` to `my-lib/src/`. It taps the `described-resolve` hook (after the description file has been located) and forwards the rewritten request to `resolve`, so the pipeline restarts with the new request. @@ -470,7 +557,7 @@ Tips for writing your own plugin: - Call `callback()` with no arguments to pass the request on to the next tapped handler at the same `source` hook. This is how you "opt out" when a request doesn't apply. - Call `resolver.doResolve(target, newRequest, message, resolveContext, callback)` to continue the pipeline at a different hook with a (possibly modified) request. - Return early with `callback(null, result)` to short-circuit with a specific result, or `callback(err)` to fail the resolve. -- Common hook names you'll see as `source`/`target`: `resolve`, `parsed-resolve`, `described-resolve`, `raw-resolve`, `normal-resolve`, `relative`, `directory`, `file`, `existing-file`, `resolved`. Read `lib/ResolverFactory.js` for the full pipeline. +- See [Hooks](#hooks) for the full list of pipeline hooks, their order, and the `before-` / `after-` stage modifiers. `lib/ResolverFactory.js` has the exact wiring under `//// pipeline ////`. ## Escaping From 3645dccea5d3880adb9e3efb61bb76190b646910 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 15:56:01 +0000 Subject: [PATCH 2/5] docs: add request-flow schemas for each request type Follow-up to the Hooks section: walks through the hook chain for a relative path, bare module, internal import, alias hit, exports-field match, and a failed resolve, so readers can see which hooks run for which kind of request without having to trace ResolverFactory.js. --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/README.md b/README.md index 726ae409..c799f647 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,84 @@ Listed roughly in the order the default pipeline visits them. Full wiring lives `ensureHook("before-foo")` and `getHook("before-foo")` return the `foo` hook with `stage: -10`; `after-foo` returns it with `stage: 10`. Use this to tap earlier or later than the default stage without creating a separate hook. You'll see `after-parsed-resolve`, `after-normal-resolve`, `after-relative`, and `after-undescribed-resolve-in-package` used this way inside `ResolverFactory`. +#### Request flow by request type + +The same 26 pipeline hooks serve every request, but different request shapes take different paths through them. Each `➝` below is one `doResolve` / `NextPlugin` / plugin forward; `resolveStep` fires on every arrow, so tapping it (see [Hook examples](#hook-examples)) prints these chains live. + +Relative path (`./utils` from `/src/index.js`) — the default "resolve on disk" path: + +```text +resolve (ParsePlugin) + ➝ parsed-resolve (DescriptionFilePlugin attaches nearest package.json) + ➝ described-resolve (NextPlugin; or UnsafeCachePlugin short-circuit) + ➝ raw-resolve (NextPlugin; alias/tsconfig would branch here) + ➝ normal-resolve (JoinRequestPlugin: path=/src/utils, request="") + ➝ relative (DescriptionFilePlugin loads /src/package.json) + ➝ described-relative (branches to file and directory candidates) + ├─ ➝ raw-file (ConditionalPlugin / TryNextPlugin) + │ ➝ file (AppendPlugin tried each extension, e.g. .js) + │ ➝ final-file (FileExistsPlugin confirms the file) + │ ➝ existing-file (SymlinkPlugin real-paths it) + │ ➝ resolved (RestrictionsPlugin → ResultPlugin) + └─ ➝ directory (DirectoryExistsPlugin; used when path is a dir) + ➝ undescribed-existing-directory + ➝ existing-directory (MainFieldPlugin tries "main", UseFilePlugin tries "index") + ➝ undescribed-raw-file ➝ raw-file ➝ … +``` + +Bare module (`lodash/merge`) — walks up `node_modules`, then treats the hit as a package: + +```text +resolve ➝ parsed-resolve ➝ described-resolve ➝ raw-resolve ➝ normal-resolve + ➝ raw-module (ConditionalPlugin {module:true}) + ➝ module (ModulesInHierarchicalDirectoriesPlugin walks + /src/node_modules, /node_modules, …) + ➝ resolve-as-module (JoinRequestPartPlugin splits "lodash" / "./merge") + ➝ undescribed-resolve-in-package (DirectoryExistsPlugin gates on lodash/ existing) + ➝ resolve-in-package (DescriptionFilePlugin loads lodash/package.json) + ├─ ➝ relative (ExportsFieldPlugin, if "exports" matches) + └─ ➝ resolve-in-existing-directory (otherwise; JoinRequestPlugin joins "./merge") + ➝ relative ➝ … (same tail as the relative flow above) +``` + +Internal import (`#util` from inside a package) — re-enters the pipeline after mapping: + +```text +resolve ➝ parsed-resolve ➝ described-resolve ➝ raw-resolve ➝ normal-resolve + ➝ internal (ConditionalPlugin {internal:true}) + ➝ imports-resolve (ImportsFieldPlugin mapped "#util" to a target) + ➝ parsed-resolve ➝ … (fresh pipeline run, internal:false so # isn't remapped) +``` + +Alias hit (`@/button` with `alias: { "@": "/src" }`) — rewrites then restarts: + +```text +resolve ➝ parsed-resolve ➝ described-resolve + ➝ raw-resolve + ➝ internal-resolve (AliasPlugin rewrote request → "/src/button") + ➝ parsed-resolve ➝ … (fullySpecified forced off; AliasPlugin won't re-fire for the rewritten form) +``` + +`exports`-field hit inside a package (`pkg/feature` matching `"./feature"` in `exports`): + +```text +… ➝ raw-module ➝ module ➝ resolve-as-module ➝ undescribed-resolve-in-package + ➝ resolve-in-package + ➝ relative (ExportsFieldPlugin jumped to the exports target; + main-field / main-file logic is skipped) + ➝ described-relative ➝ raw-file ➝ file ➝ final-file ➝ existing-file ➝ resolved +``` + +Failure — every candidate opts out (`callback()`) and no handler ever short-circuits with a result. `noResolve` fires once, for the top-level request: + +```text +… ➝ final-file + ✗ FileExistsPlugin: ENOENT (opts out; no extension candidates left) + ⇠ bail hooks unwind, each tapped handler has already tried its alternatives + ⇒ top-level resolve() returns no result + ⇒ resolver.hooks.noResolve(request, error) (ResultPlugin never fires) +``` + #### Hook examples Trace every pipeline step and observe failures via the lifecycle hooks: From a7c856f9f962216f6d39a5978e6e65ccf7bd5f8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 16:20:06 +0000 Subject: [PATCH 3/5] docs: add per-plugin goal and hook wiring notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For each of the 29 built-in plugins, adds a one-line goal plus the hook it taps and where it forwards — so readers of the Built-in Plugins table can see how each plugin plugs into the hook pipeline. --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index c799f647..1e0b83f8 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,40 @@ Plugins are executed in a pipeline, and register which event they should be exec | `UnsafeCachePlugin` | Caches successful resolves in an in-memory map to speed up repeated requests. Powers `unsafeCache`. | | `UseFilePlugin` | Joins a fixed filename onto the current path (e.g. `index`). Powers `mainFiles`. | +#### Plugin wiring and goals + +One-line goal and default wiring (`source → target`) for each plugin. `*` means the plugin is tapped on several hooks — the common ones are listed. Plugins without a fixed wiring are user-tapped. + +- **`AliasPlugin`** — Goal: redirect requests matching a configured key to an alternative target. `raw-resolve` → `internal-resolve` for `alias`; `file` → `internal-resolve` as a last-chance remap; `described-resolve` → `internal-resolve` for `fallback`. +- **`AliasFieldPlugin`** — Goal: apply aliases declared in a description-file field like `browser`, so environment-specific substitutions happen without user config. `raw-resolve` / `file` → `internal-resolve`. +- **`AppendPlugin`** — Goal: try appending a fixed string (usually an extension) to the current path. `raw-file` → `file`, one instance per entry in `extensions`. +- **`CloneBasenamePlugin`** — Goal: join the directory's basename onto the path (e.g. `/foo/bar` → `/foo/bar/bar`) for directory-named-main layouts. User-wired via `plugins`. +- **`ConditionalPlugin`** — Goal: gate a forward on a partial match of the request shape (e.g. `{ module: true }`), used to fan-out at branching hooks. Tapped on `after-normal-resolve`, `resolve-as-module`, `described-relative`, and `raw-file`. +- **`DescriptionFilePlugin`** — Goal: locate and attach the nearest description file (usually `package.json`) so downstream plugins can read its fields. `parsed-resolve` → `described-resolve`, `relative` → `described-relative`, `undescribed-resolve-in-package` → `resolve-in-package`, `undescribed-existing-directory` → `existing-directory`, `undescribed-raw-file` → `raw-file`. +- **`DirectoryExistsPlugin`** — Goal: only continue the pipeline if the current path exists as a directory. `resolve-as-module` → `undescribed-resolve-in-package`, `directory` → `undescribed-existing-directory`. +- **`ExportsFieldPlugin`** — Goal: map a request through the `exports` field of a package's description file (with `conditionNames`). `resolve-in-package` → `relative`. +- **`ExtensionAliasPlugin`** — Goal: rewrite a request's extension to a list of candidate extensions (e.g. `.js` → `.ts`, `.js`) for TypeScript ESM and similar. `raw-resolve` → `normal-resolve`. +- **`FileExistsPlugin`** — Goal: confirm a candidate path exists as a file and record it as a file dependency. `final-file` → `existing-file`. +- **`ImportsFieldPlugin`** — Goal: resolve `#name` requests through the `imports` field of the enclosing package. `internal` → `relative` (relative target) or `imports-resolve` (bare target). +- **`JoinRequestPlugin`** — Goal: join the current path with the current request into a single concrete path. `after-normal-resolve` → `relative` (three stage-offset copies for `preferRelative`, `preferAbsolute`, and default), `resolve-in-existing-directory` → `relative`. +- **`JoinRequestPartPlugin`** — Goal: split a module request into module name + inner request, joining the inner part onto the path. `module` → `resolve-as-module`. +- **`LogInfoPlugin`** — Goal: emit verbose log output at a chosen hook; enable by passing a `log` function on `resolveContext`. User-wired via `plugins`. +- **`MainFieldPlugin`** — Goal: follow a description-file field (e.g. `main`, `module`, `browser`) to the entry file of a package. `existing-directory` → `resolve-in-existing-directory`, one instance per entry in `mainFields`. +- **`ModulesInHierarchicalDirectoriesPlugin`** — Goal: search for a module by walking up parent directories (the standard `node_modules` lookup). `raw-module` → `module`; when PnP is enabled, `alternate-raw-module` → `module` too. +- **`ModulesInRootPlugin`** — Goal: search for a module in a single absolute directory (powers absolute-path entries in `modules`). `raw-module` → `module`. +- **`NextPlugin`** — Goal: glue — forward the current request unchanged from one hook to another. Used across the pipeline wherever two hooks should run sequentially. +- **`ParsePlugin`** — Goal: split the raw request string into path / query / fragment / `module` / `directory` / `internal` flags. `resolve` → `parsed-resolve`; also wired on `internal-resolve` and `imports-resolve`. +- **`PnpPlugin`** — Goal: resolve bare-module requests through Yarn's PnP API when available. `raw-module` → `undescribed-resolve-in-package` on hit, `alternate-raw-module` on miss. +- **`RestrictionsPlugin`** — Goal: reject resolved paths that don't satisfy at least one string prefix or RegExp. Tapped on `resolved`. +- **`ResultPlugin`** — Goal: terminal plugin — fires the `result` lifecycle hook and signals a successful resolve. Tapped on `resolved`. +- **`RootsPlugin`** — Goal: resolve server-relative URL requests (starting with `/`) against one or more root directories. `after-normal-resolve` → `relative`. +- **`SelfReferencePlugin`** — Goal: resolve a package self-reference (`my-pkg/foo` from inside `my-pkg`) via its own `exports`. `raw-module` → `resolve-as-module`. +- **`SymlinkPlugin`** — Goal: real-path the resolved file by following symlinks; can be disabled via `symlinks: false`. `existing-file` → `existing-file` (runs via a stage offset on the same hook). +- **`TryNextPlugin`** — Goal: forward the request to another hook with a log message, useful for trying an alternative candidate. `raw-file` → `file` (as the "no extension" attempt) and user-wired. +- **`TsconfigPathsPlugin`** — Goal: rewrite requests using the `paths` and `baseUrl` from a `tsconfig.json` (including project references). Taps `described-resolve` internally and forwards to `internal-resolve`; exported for direct use as well. +- **`UnsafeCachePlugin`** — Goal: cache successful resolves in an in-memory map for repeated requests. `described-resolve` → `raw-resolve` (only when `unsafeCache` is enabled). +- **`UseFilePlugin`** — Goal: join a fixed filename (e.g. `index`) onto the current path to try as an entry file. `existing-directory` / `undescribed-existing-directory` → `undescribed-raw-file`, one instance per entry in `mainFiles`. + ### Hooks A resolver exposes two kinds of [`tapable`](https://github.com/webpack/tapable) hooks: From 563f557e3f9825a3cfbc22724e1f3f2db5d6ef67 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 21 Apr 2026 17:01:05 +0300 Subject: [PATCH 4/5] style: fix --- README.md | 70 +++++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 1e0b83f8..06bfd02c 100644 --- a/README.md +++ b/README.md @@ -469,45 +469,45 @@ A resolver exposes two kinds of [`tapable`](https://github.com/webpack/tapable) #### Lifecycle hooks -| Hook | Type | Fires when | -| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `resolveStep` | `SyncHook` | Every time the resolver hands a request to a pipeline hook. Arguments: `(hook, request)`. Ideal for tracing. | -| `noResolve` | `SyncHook` | When a top-level `resolve()` call can't produce a result. Arguments: `(request, error)`. | -| `resolve` | `AsyncSeriesBailHook`| Entry point of the pipeline (also listed below). Tap this to intercept requests before parsing. | -| `result` | `AsyncSeriesHook` | After a successful resolve, with the final request. Fired by `ResultPlugin`. Tap to observe/record results without short-circuiting. | +| Hook | Type | Fires when | +| ------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `resolveStep` | `SyncHook` | Every time the resolver hands a request to a pipeline hook. Arguments: `(hook, request)`. Ideal for tracing. | +| `noResolve` | `SyncHook` | When a top-level `resolve()` call can't produce a result. Arguments: `(request, error)`. | +| `resolve` | `AsyncSeriesBailHook` | Entry point of the pipeline (also listed below). Tap this to intercept requests before parsing. | +| `result` | `AsyncSeriesHook` | After a successful resolve, with the final request. Fired by `ResultPlugin`. Tap to observe/record results without short-circuiting. | #### Pipeline hooks Listed roughly in the order the default pipeline visits them. Full wiring lives in `lib/ResolverFactory.js` under `//// pipeline ////`. -| Hook | Role | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `resolve` | Entry point. `ParsePlugin` parses the raw request (path, query, fragment, module flag) and forwards to `parsed-resolve`. | -| `internal-resolve` | Re-entry point used by internal rewrites (e.g. after an `alias` fires). Same role as `resolve`, but `fullySpecified` is forced off. | -| `imports-resolve` | Re-entry point for the target of an `imports` field match; prevents recursive `#` resolution per the Node.js ESM spec. | -| `parsed-resolve` | Request has been parsed. `DescriptionFilePlugin` attaches the nearest `package.json`, then forwards to `described-resolve`. | -| `described-resolve` | Description file is attached. Where `unsafeCache`, `fallback`, and most user plugins (including `MyLibSrcPlugin` below) hook in. | -| `raw-resolve` | After description. Where `alias`, `aliasFields`, `tsconfig` paths, and `extensionAlias` rewrites fire before default resolution. | -| `normal-resolve` | Default resolution starts. Branches into `relative` (for `./`, `../`, absolute), `raw-module` (bare modules), or `internal` (`#imports`). | -| `internal` | `#name` imports-field entry. `ImportsFieldPlugin` maps the specifier and forwards to `relative` or `imports-resolve`. | -| `raw-module` | Bare-module lookup. `SelfReferencePlugin`, `ModulesInHierarchicalDirectoriesPlugin`, `ModulesInRootPlugin`, and `PnpPlugin` all tap here. | -| `alternate-raw-module` | Fallback module lookup used by `PnpPlugin` when PnP can't resolve and `node_modules` should be tried. | -| `module` | A candidate module directory was built. `JoinRequestPartPlugin` splits off the inner request and forwards to `resolve-as-module`. | -| `resolve-as-module` | Treat candidate as a package. `DirectoryExistsPlugin` gates on existence; short single-file modules may re-enter via `undescribed-raw-file`. | -| `undescribed-resolve-in-package` | Inside a located package directory, before its `package.json` has been read. Loads the description, forwards to `resolve-in-package`. | -| `resolve-in-package` | Inside a package with its description loaded. `ExportsFieldPlugin` matches `exports`, otherwise forwards to `resolve-in-existing-directory`. | -| `resolve-in-existing-directory` | Package directory confirmed; join the remaining request onto it and continue at `relative`. | -| `relative` | A concrete path on disk. `DescriptionFilePlugin` loads the nearest `package.json` and forwards to `described-relative`. | -| `described-relative` | Branches to `raw-file` (treat as file) and `directory` (treat as directory). `resolveToContext` skips the file branch. | -| `directory` | Candidate directory. `DirectoryExistsPlugin` gates on existence and forwards to `undescribed-existing-directory`. | -| `undescribed-existing-directory` | Existing directory, before its `package.json` has been read. `UseFilePlugin` tries `mainFiles` via `undescribed-raw-file`. | -| `existing-directory` | Existing directory with description loaded. `MainFieldPlugin` tries `mainFields`; `UseFilePlugin` falls back to `mainFiles`. | -| `undescribed-raw-file` | Candidate file path, before description is read. Loads description, then forwards to `raw-file`. | -| `raw-file` | Apply extension handling: `ConditionalPlugin` short-circuits when `fullySpecified`, `TryNextPlugin` + `AppendPlugin` try each extension. | -| `file` | A specific file path. Last place `alias` and `aliasFields` can redirect; forwards to `final-file`. | -| `final-file` | `FileExistsPlugin` checks the file is real and records it as a file dependency, then forwards to `existing-file`. | -| `existing-file` | Real file on disk. `SymlinkPlugin` real-paths symlinks (unless `symlinks: false`), then forwards to `resolved`. | -| `resolved` | Terminal hook. `RestrictionsPlugin` enforces `restrictions`; `ResultPlugin` fires the `result` lifecycle hook. | +| Hook | Role | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `resolve` | Entry point. `ParsePlugin` parses the raw request (path, query, fragment, module flag) and forwards to `parsed-resolve`. | +| `internal-resolve` | Re-entry point used by internal rewrites (e.g. after an `alias` fires). Same role as `resolve`, but `fullySpecified` is forced off. | +| `imports-resolve` | Re-entry point for the target of an `imports` field match; prevents recursive `#` resolution per the Node.js ESM spec. | +| `parsed-resolve` | Request has been parsed. `DescriptionFilePlugin` attaches the nearest `package.json`, then forwards to `described-resolve`. | +| `described-resolve` | Description file is attached. Where `unsafeCache`, `fallback`, and most user plugins (including `MyLibSrcPlugin` below) hook in. | +| `raw-resolve` | After description. Where `alias`, `aliasFields`, `tsconfig` paths, and `extensionAlias` rewrites fire before default resolution. | +| `normal-resolve` | Default resolution starts. Branches into `relative` (for `./`, `../`, absolute), `raw-module` (bare modules), or `internal` (`#imports`). | +| `internal` | `#name` imports-field entry. `ImportsFieldPlugin` maps the specifier and forwards to `relative` or `imports-resolve`. | +| `raw-module` | Bare-module lookup. `SelfReferencePlugin`, `ModulesInHierarchicalDirectoriesPlugin`, `ModulesInRootPlugin`, and `PnpPlugin` all tap here. | +| `alternate-raw-module` | Fallback module lookup used by `PnpPlugin` when PnP can't resolve and `node_modules` should be tried. | +| `module` | A candidate module directory was built. `JoinRequestPartPlugin` splits off the inner request and forwards to `resolve-as-module`. | +| `resolve-as-module` | Treat candidate as a package. `DirectoryExistsPlugin` gates on existence; short single-file modules may re-enter via `undescribed-raw-file`. | +| `undescribed-resolve-in-package` | Inside a located package directory, before its `package.json` has been read. Loads the description, forwards to `resolve-in-package`. | +| `resolve-in-package` | Inside a package with its description loaded. `ExportsFieldPlugin` matches `exports`, otherwise forwards to `resolve-in-existing-directory`. | +| `resolve-in-existing-directory` | Package directory confirmed; join the remaining request onto it and continue at `relative`. | +| `relative` | A concrete path on disk. `DescriptionFilePlugin` loads the nearest `package.json` and forwards to `described-relative`. | +| `described-relative` | Branches to `raw-file` (treat as file) and `directory` (treat as directory). `resolveToContext` skips the file branch. | +| `directory` | Candidate directory. `DirectoryExistsPlugin` gates on existence and forwards to `undescribed-existing-directory`. | +| `undescribed-existing-directory` | Existing directory, before its `package.json` has been read. `UseFilePlugin` tries `mainFiles` via `undescribed-raw-file`. | +| `existing-directory` | Existing directory with description loaded. `MainFieldPlugin` tries `mainFields`; `UseFilePlugin` falls back to `mainFiles`. | +| `undescribed-raw-file` | Candidate file path, before description is read. Loads description, then forwards to `raw-file`. | +| `raw-file` | Apply extension handling: `ConditionalPlugin` short-circuits when `fullySpecified`, `TryNextPlugin` + `AppendPlugin` try each extension. | +| `file` | A specific file path. Last place `alias` and `aliasFields` can redirect; forwards to `final-file`. | +| `final-file` | `FileExistsPlugin` checks the file is real and records it as a file dependency, then forwards to `existing-file`. | +| `existing-file` | Real file on disk. `SymlinkPlugin` real-paths symlinks (unless `symlinks: false`), then forwards to `resolved`. | +| `resolved` | Terminal hook. `RestrictionsPlugin` enforces `restrictions`; `ResultPlugin` fires the `result` lifecycle hook. | #### `before-` and `after-` prefixes @@ -617,7 +617,7 @@ class StubCssPlugin { .getHook("file") .tapAsync("StubCssPlugin", (request, _ctx, callback) => { if (!request.path || !request.path.endsWith(".css")) return callback(); - callback(null, { ...request, path: require.resolve("./empty.js") }); + callback(null, { ...request, path: require.resolve("./empty.css") }); }); } } From 78ea2d82af9ab14f2d70322e95d9476efdaccc5c Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 21 Apr 2026 17:08:39 +0300 Subject: [PATCH 5/5] chore: fix cspell --- .cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index 02e19848..df562c03 100644 --- a/.cspell.json +++ b/.cspell.json @@ -48,7 +48,8 @@ "Sindre", "Sorhus", "readlink", - "extensionless" + "extensionless", + "ENOENT" ], "ignorePaths": [ "package.json",