-
-
Notifications
You must be signed in to change notification settings - Fork 428
WIP: chore: validate rspack version with manifest #4887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@module-federation/manifest": patch | ||
| --- | ||
|
|
||
| Use Rspack's built-in Module Federation manifest and require Rspack 1.7.12 or 2.0.0 and above when manifests are enabled. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import type { Compiler } from 'webpack'; | ||
| import type { moduleFederationPlugin } from '@module-federation/sdk'; | ||
|
|
||
| jest.mock( | ||
| '@module-federation/sdk', | ||
| () => ({ | ||
| bindLoggerToCompiler: jest.fn(), | ||
| moduleFederationPlugin: {}, | ||
| }), | ||
| { virtual: true }, | ||
| ); | ||
|
|
||
| jest.mock('../src/logger', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| error: jest.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| jest.mock('../src/StatsManager', () => ({ | ||
| StatsManager: class { | ||
| fileName = 'mf-stats.json'; | ||
|
|
||
| init = jest.fn(); | ||
| validate = jest.fn(() => true); | ||
| updateStats = jest.fn((stats) => stats); | ||
| generateStats = jest.fn(async () => ({ generatedBy: 'webpack' })); | ||
| getPublicPath = jest.fn(() => '/'); | ||
| }, | ||
| })); | ||
|
|
||
| jest.mock('../src/ManifestManager', () => ({ | ||
| ManifestManager: class { | ||
| fileName = 'mf-manifest.json'; | ||
|
|
||
| init = jest.fn(); | ||
| updateManifest = jest.fn(() => ({ generatedBy: 'rspack' })); | ||
| generateManifest = jest.fn(() => ({ generatedBy: 'webpack' })); | ||
| }, | ||
| })); | ||
|
|
||
| import { StatsPlugin } from '../src/StatsPlugin'; | ||
|
|
||
| const createCompiler = (rspackVersion?: string) => { | ||
| class RawSource { | ||
| constructor(readonly value: string) {} | ||
| } | ||
|
|
||
| return { | ||
| webpack: { | ||
| rspackVersion, | ||
| sources: { RawSource }, | ||
| }, | ||
| options: { | ||
| output: { publicPath: 'auto' }, | ||
| }, | ||
| hooks: { | ||
| thisCompilation: { | ||
| tap: jest.fn(), | ||
| }, | ||
| }, | ||
| } as unknown as Compiler; | ||
| }; | ||
|
|
||
| const createPlugin = ( | ||
| bundler: 'webpack' | 'rspack', | ||
| manifest: moduleFederationPlugin.ModuleFederationPluginOptions['manifest'] = true, | ||
| ) => | ||
| new StatsPlugin( | ||
| { | ||
| name: 'host', | ||
| manifest, | ||
| }, | ||
| { | ||
| pluginVersion: '1.0.0', | ||
| bundler, | ||
| }, | ||
| ); | ||
|
|
||
| const getProcessAssetsHandler = (compiler: Compiler, getAsset: jest.Mock) => { | ||
| const processAssets = { | ||
| tapPromise: jest.fn(), | ||
| }; | ||
| const compilation = { | ||
| constructor: { | ||
| PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER: 100, | ||
| }, | ||
| hooks: { processAssets }, | ||
| getAsset, | ||
| updateAsset: jest.fn(), | ||
| emitAsset: jest.fn(), | ||
| }; | ||
|
|
||
| const thisCompilationHandler = ( | ||
| compiler.hooks.thisCompilation.tap as jest.Mock | ||
| ).mock.calls[0][1]; | ||
| thisCompilationHandler(compilation); | ||
|
|
||
| return { | ||
| compilation, | ||
| handler: processAssets.tapPromise.mock.calls[0][1] as () => Promise<void>, | ||
| }; | ||
| }; | ||
|
|
||
| describe('StatsPlugin', () => { | ||
| it.each(['1.3.9', '1.7.11', '1.7.12-beta.0', '2.0.0-beta.1'])( | ||
| 'requires a Rspack upgrade when manifest is enabled on %s', | ||
| (version) => { | ||
| const compiler = createCompiler(version); | ||
|
|
||
| expect(() => createPlugin('rspack').apply(compiler)).toThrow( | ||
| /upgrade to Rspack 1\.7\.12 or 2\.0\.0 and above\.$/, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| it.each(['1.7.12', '1.8.0', '2.0.0', '2.1.2'])( | ||
| 'accepts Rspack %s built-in manifest support', | ||
| (version) => { | ||
| const compiler = createCompiler(version); | ||
|
|
||
| expect(() => createPlugin('rspack').apply(compiler)).not.toThrow(); | ||
| expect(compiler.hooks.thisCompilation.tap).toHaveBeenCalledTimes(1); | ||
| }, | ||
| ); | ||
|
|
||
| it('does not require a supported Rspack version when manifest is disabled', () => { | ||
| const compiler = createCompiler('1.3.9'); | ||
|
|
||
| expect(() => createPlugin('rspack', false).apply(compiler)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('does not apply the Rspack version requirement to webpack', () => { | ||
| const compiler = createCompiler(); | ||
|
|
||
| expect(() => createPlugin('webpack').apply(compiler)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('uses Rspack built-in stats without running the webpack generator', async () => { | ||
| const compiler = createCompiler('1.7.12'); | ||
| createPlugin('rspack').apply(compiler); | ||
| const source = { | ||
| source: () => JSON.stringify({ generatedBy: 'rspack' }), | ||
| }; | ||
| const { compilation, handler } = getProcessAssetsHandler( | ||
| compiler, | ||
| jest.fn(() => ({ source })), | ||
| ); | ||
|
|
||
| await handler(); | ||
|
|
||
| expect(compilation.updateAsset).toHaveBeenCalledTimes(2); | ||
| expect(compilation.emitAsset).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not fall back when Rspack built-in stats are missing', async () => { | ||
| const compiler = createCompiler('2.0.0'); | ||
| createPlugin('rspack').apply(compiler); | ||
| const { compilation, handler } = getProcessAssetsHandler( | ||
| compiler, | ||
| jest.fn(() => undefined), | ||
| ); | ||
|
|
||
| await expect(handler()).rejects.toThrow( | ||
| /Rspack's built-in manifest did not emit mf-stats\.json/, | ||
| ); | ||
| expect(compilation.emitAsset).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('keeps webpack manifest generation unchanged', async () => { | ||
| const compiler = createCompiler(); | ||
| createPlugin('webpack').apply(compiler); | ||
| const { compilation, handler } = getProcessAssetsHandler( | ||
| compiler, | ||
| jest.fn(() => undefined), | ||
| ); | ||
|
|
||
| await handler(); | ||
|
|
||
| expect(compilation.emitAsset).toHaveBeenCalledTimes(2); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,36 @@ import { StatsManager } from './StatsManager'; | |
| import { PLUGIN_IDENTIFIER } from './constants'; | ||
| import logger from './logger'; | ||
|
|
||
| const isRspackManifestSupported = (version?: string): boolean => { | ||
| if (!version) { | ||
| return false; | ||
| } | ||
|
|
||
| const match = | ||
| /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec( | ||
| version, | ||
| ); | ||
|
|
||
| if (!match) { | ||
| return false; | ||
| } | ||
|
|
||
| const [, majorValue, minorValue, patchValue, prerelease] = match; | ||
| const current = [Number(majorValue), Number(minorValue), Number(patchValue)]; | ||
| const minimum = current[0] === 1 ? [1, 7, 12] : [2, 0, 0]; | ||
|
|
||
| for (let index = 0; index < current.length; index += 1) { | ||
| if (current[index] > minimum[index]) { | ||
| return true; | ||
| } | ||
| if (current[index] < minimum[index]) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return !prerelease; | ||
| }; | ||
|
|
||
| export class StatsPlugin implements WebpackPluginInstance { | ||
| readonly name = 'StatsPlugin'; | ||
| private _options: moduleFederationPlugin.ModuleFederationPluginOptions = {}; | ||
|
|
@@ -42,6 +72,21 @@ export class StatsPlugin implements WebpackPluginInstance { | |
| if (!this._enable) { | ||
| return; | ||
| } | ||
|
|
||
| if (this._bundler === 'rspack' && this._options.manifest !== false) { | ||
| const rspackVersion = ( | ||
| compiler.webpack as typeof compiler.webpack & { | ||
| rspackVersion?: string; | ||
| } | ||
| ).rspackVersion; | ||
|
|
||
| if (!isRspackManifestSupported(rspackVersion)) { | ||
| throw new Error( | ||
| `[ ${PLUGIN_IDENTIFIER} ]: Rspack ${rspackVersion || 'unknown'} does not support the required built-in manifest capability. Please upgrade to Rspack 1.7.12 or 2.0.0 and above.`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| const res = this._statsManager.validate(compiler); | ||
|
|
||
| if (!res) { | ||
|
|
@@ -59,7 +104,7 @@ export class StatsPlugin implements WebpackPluginInstance { | |
| const existedStats = compilation.getAsset( | ||
| this._statsManager.fileName, | ||
| ); | ||
| // new rspack should hit | ||
| // Rspack's built-in manifest is the only source of Rspack stats. | ||
| if (existedStats) { | ||
| let updatedStats = this._statsManager.updateStats( | ||
| JSON.parse(existedStats.source.source().toString()), | ||
|
|
@@ -99,7 +144,13 @@ export class StatsPlugin implements WebpackPluginInstance { | |
| return; | ||
| } | ||
|
|
||
| // webpack + legacy rspack | ||
| if (this._bundler === 'rspack') { | ||
|
Comment on lines
144
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With Rspack configs that rely on this wrapper's longstanding default ( Useful? React with 👍 / 👎. |
||
| throw new Error( | ||
| `[ ${PLUGIN_IDENTIFIER} ]: Rspack's built-in manifest did not emit ${this._statsManager.fileName}.`, | ||
| ); | ||
| } | ||
|
|
||
| // webpack | ||
| let stats = await this._statsManager.generateStats( | ||
| compiler, | ||
| compilation, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This guard makes every Rspack manifest build fail below 1.7.12, but the workspace still resolves existing manifest/demo Rspack apps to older
@rspack/coreversions (for exampleapps/manifest-demo/3010-rspack-provideris locked to 1.3.9 inpnpm-lock.yaml), and I checked.github/workflows/devtools.yml: it runspnpm run app:manifest:devandapp:manifest:prod. As committed, those CI jobs throw during plugin application before tests start; update the affected package specs/lockfile or opt those configs out of manifest generation along with this guard.Useful? React with 👍 / 👎.