test(react-icons-atomic-webpack-loader): verify fixtures against rspack - #1224
Draft
Martin Hochel (Hotell) wants to merge 6 commits into
Draft
test(react-icons-atomic-webpack-loader): verify fixtures against rspack#1224Martin Hochel (Hotell) wants to merge 6 commits into
Martin Hochel (Hotell) wants to merge 6 commits into
Conversation
The plugin crashed under rspack with "Compilation.hooks.optimizeAssets is not supported in rspack", and would have silently subset nothing even with the hook fixed. Six distinct incompatibilities are addressed: - static `webpack` runtime import -> type-only import + `compiler.webpack` - `hooks.optimizeAssets` -> `hooks.processAssets` at PROCESS_ASSETS_STAGE_OPTIMIZE - `instanceof NormalModule` -> duck-typing on `resource` (rspack modules are napi proxies and never satisfy the webpack class) - `getUsedExports(m, undefined)` -> explicit runtime, since rspack's binding requires `string | string[]` - `buildInfo.filename` -> `AssetInfo.sourceFilename`, as rspack leaves `buildInfo` empty for asset modules - direct `compilation.assets` mutation -> `updateAsset`, as rspack's assets record is a read-only proxy A warning is now emitted when font modules are found but no asset can be mapped, so a future incompatibility surfaces instead of silently shipping a full font. Tests run the same five fixture entries against both bundlers via a shared config factory; emitted font sizes are identical across webpack and rspack. `webpack` and `@rspack/core` are both optional peer dependencies.
… optional peer deps
…ut of public API Review feedback: `import type * as webpack from 'webpack'` was preserved in the generated `lib/index.d.ts`, so a consumer who installed only `@rspack/core` — which the optional peerDependencies explicitly allow — would hit `Cannot find module 'webpack'` when type-checking. Replaces the webpack namespace types with a minimal structural description of the handful of APIs the plugin actually uses. These stay module-private, so nothing reaches the emitted declaration file. A fully structural public signature turned out to break assignability to webpack's `plugins` array (its `Module` shares no properties with a structural stand-in, and `hooks.compilation.tap` has a different signature), so `apply()` takes `unknown` and narrows once. That keeps the plugin assignable to both bundlers while the internals stay precisely typed. Also drops the `as never` cast on `getUsedExports`: the local `ModuleGraph` type now declares the runtime parameter as `string[] | undefined`, covering both bundlers without disabling type-checking at the callsite. `webpack` joins `@rspack/core` in the dependency-checks ignore list, since neither is imported any more, not even for types.
… plugin contract
Replaces `apply(compiler: unknown)` plus an internal assertion with
`implements BundlerPlugin` and a typed `apply(compiler: BundlerCompiler)`.
The earlier `unknown` was a workaround for the maintained interfaces not being
supertypes of the real bundler types. `apply` is a property on both bundlers'
plugin interfaces, so `strictFunctionTypes` requires contravariance: every real
Compiler must be assignable to ours. Two things blocked that:
- `Module` declared only optional members, so webpack's `Module` tripped weak
type detection ("no properties in common"). Adding `type` fixes it, since both
bundlers expose it.
- `getUsedExports`/`getProvidedExports` were narrower than webpack's `RuntimeSpec`
and `readonly string[]` returns.
`updateAsset` keeps an `any` parameter, documented inline: both bundlers accept a
full webpack-sources `Source` there and only `any` is assignable to it.
Adds `test/types.conformance.ts`, checked by tsc via the `test` script, asserting
that both bundlers' real `Compiler` types still satisfy the maintained interfaces
and that the plugin still satisfies `WebpackPluginInstance` and
`RspackPluginInstance`. This is the drift protection those hand-written types
otherwise lacked; narrowing any of them fails the check.
… types to own module Moves the hand-maintained bundler interfaces out of `index.ts` into `src/bundler-api.ts`, so the plugin module contains only subsetting logic and the bundler surface is encapsulated in one place alongside the rationale for keeping it hand-written. `index.ts` re-exports the module (`export type * from './bundler-api'`), so the package's public type surface is unchanged for consumers. `NormalModule` becomes `BundlerNormalModule` now that it is exported, and the inline `Compiler['webpack']['sources']['RawSource']` lookup becomes a named `BundlerRawSource` alias. `test/types.conformance.ts` now imports the interfaces from the new module, so the drift check targets it directly.
Mirrors the harness added for the font subsetting plugin: the fixture entries and
their assertions move into a shared `test/make-configs.js`, and `test/run.js`
(`--bundler webpack|rspack|all`) drives both bundlers over the same 17 entries.
All 17 pass under rspack with no per-bundler assertion overrides, including the
`ts-loader` rule, `experiments.outputModule` with `library: { type: 'module' }`,
and the emitted-warning assertions. The rspack config deliberately keeps the same
ts-loader setup as webpack so any difference would come from the bundler rather
than the TypeScript toolchain. `make-configs.js` supports `overrides.<bundler>`
should a future divergence need it.
One behavioural fix was required: the assertion plugin read warnings via
`w instanceof Error ? w.message : String(w)`, and rspack surfaces RspackError
objects rather than Error instances, which would have stringified to garbage
instead of failing. It now reads `message` directly.
Also drops `import type { LoaderContext } from 'webpack'`, which was preserved in
`lib/index.d.ts` and would break type-checking for consumers who install only
`@rspack/core` now that both are optional peers. Replaced by a hand-maintained
`AtomicLoaderContext` covering the four members the loader uses, with
`test/types.conformance.ts` asserting both bundlers' real loader contexts still
satisfy it.
📋 PR Validation SummaryCheck the Build react library job summary for detailed reports:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
@fluentui/react-icons-atomic-webpack-loaderwas only ever exercised against webpack. Since we'renow claiming rspack support across the icon tooling, that claim should be tested rather than assumed.
Change
The fixture entries and their assertions move into a shared
test/make-configs.js, andtest/run.js(--bundler webpack|rspack|all) drives both bundlers over the same 17 entries.test/webpack.config.jsbecomes a thin wrapper, so the existing webpack behaviour is unchanged.The rspack config deliberately keeps the same
ts-loadersetup as webpack rather than switchingto
builtin:swc-loader, so that any difference in emitted output comes from the bundler rather thanfrom a different TypeScript toolchain.
Result: no per-bundler overrides were needed
All 17 entries pass under rspack unmodified, including the three things I expected to be risky:
ts-loaderunder rspack (.jsx/.tsxfixtures)experiments.outputModule+library: { type: 'module' }mustInclude/mustExcludestring assertion holdsdynamic-barrel-imports)make-configs.jsstill supportsoverrides.<bundler>so a future divergence has somewhere to go,but nothing uses it today.
One real bug found
The assertion plugin read warnings as:
rspack surfaces
RspackErrorobjects rather thanErrorinstances, so this would have fallen intothe
String(w)branch and produced[object Object]— themustWarnassertions would have failedconfusingly rather than reporting the real warning. Now reads
messagedirectly, which is correctfor both bundlers.
Type leak fixed
lib/index.d.tscarriedimport type { LoaderContext } from 'webpack'. With both bundlers nowdeclared as optional peers, an rspack-only consumer would hit
Cannot find module 'webpack'while type-checking — the same defect fixed in #1219 for the font plugin.
Replaced with a hand-maintained
AtomicLoaderContextcovering the four members the loader actuallyuses (
resourcePath,getOptions,callback,emitWarning), plustest/types.conformance.tsasserting that both bundlers' realLoaderContextstill satisfiesit. Verified the check is not vacuous: adding a member no bundler has fails compilation for both.
Verification
nx run …:test— vitest + type conformance + webpack 17/17 + rspack 17/17mustIncludefails under rspack with[rspack/svg-imports] Expected output to contain "THIS-SHOULD-NOT-EXIST", proving the rspack pathactually evaluates assertions rather than passing vacuously
dynamic-barrel-imports)nx run …:lint,yarn deps:check, andyarn install --immutableall pass