From 81936bb88365de1c452e2d54d7e3cae1c64a3118 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Fri, 3 Jul 2026 23:38:48 +0100 Subject: [PATCH 1/2] feat: add rspack version detection helpers Utilities for the upcoming dual rspack-major support (see agent_context/rspackv2-jul2026/): getRspackVersion / getRspackMajorVersion / isRspack2 resolve @rspack/core/package.json without importing the package - safe on any Node version (@rspack/core@2 is ESM-only and cannot be require()d on Node < 20.19) and safe when rspack is not installed (webpack-only projects). getRspackMajorVersionFromCompiler covers plugin contexts via compiler.webpack.rspackVersion. ConfigKeys additions (cache, experiments) describe config keys the commands already read. No behavior change; nothing consumes the helpers yet. --- packages/repack/src/commands/types.ts | 2 + packages/repack/src/helpers/index.ts | 1 + packages/repack/src/helpers/rspackVersion.ts | 61 ++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 packages/repack/src/helpers/rspackVersion.ts diff --git a/packages/repack/src/commands/types.ts b/packages/repack/src/commands/types.ts index 2cdc7b18e..d62d0ad9a 100644 --- a/packages/repack/src/commands/types.ts +++ b/packages/repack/src/commands/types.ts @@ -63,7 +63,9 @@ export type RemoveRecord = T extends infer U & Record type ConfigKeys = | 'name' + | 'cache' | 'context' + | 'experiments' | 'mode' | 'devServer' | 'entry' diff --git a/packages/repack/src/helpers/index.ts b/packages/repack/src/helpers/index.ts index 692f41e48..04c5885ee 100644 --- a/packages/repack/src/helpers/index.ts +++ b/packages/repack/src/helpers/index.ts @@ -1,2 +1,3 @@ export * from './errors.js'; export * from './helpers.js'; +export * from './rspackVersion.js'; diff --git a/packages/repack/src/helpers/rspackVersion.ts b/packages/repack/src/helpers/rspackVersion.ts new file mode 100644 index 000000000..1cca1d7f7 --- /dev/null +++ b/packages/repack/src/helpers/rspackVersion.ts @@ -0,0 +1,61 @@ +import type { Compiler as RspackCompiler } from '@rspack/core'; +import type { Compiler as WebpackCompiler } from 'webpack'; + +/** + * Reads the version of the installed `@rspack/core` package without loading it. + * + * Resolving `@rspack/core/package.json` instead of importing the package keeps + * this helper safe to call in any context: + * - `@rspack/core` v2 is ESM-only, so a CJS `require('@rspack/core')` throws + * `ERR_REQUIRE_ESM` on Node < 20.19, + * - `@rspack/core` is an optional peer dependency and may not be installed + * at all (webpack-only projects). + * + * @param context Optional directory to resolve `@rspack/core` from. Defaults + * to Re.Pack's own resolution paths. + * @returns Version string or `null` when `@rspack/core` is not resolvable. + */ +export function getRspackVersion(context?: string): string | null { + try { + const options = context ? { paths: [context] } : undefined; + const packageJsonPath = require.resolve( + '@rspack/core/package.json', + options + ); + const { version }: { version: string } = require(packageJsonPath); + return version; + } catch { + return null; + } +} + +/** + * Major version of the installed `@rspack/core` package, + * or `null` when it's not resolvable. + */ +export function getRspackMajorVersion(context?: string): number | null { + const version = getRspackVersion(context); + if (!version) return null; + return Number(version.split('.')[0]); +} + +/** + * Whether the installed `@rspack/core` package is version 2 or newer. + * Returns `false` when `@rspack/core` is not resolvable. + */ +export function isRspack2(context?: string): boolean { + return (getRspackMajorVersion(context) ?? 0) >= 2; +} + +/** + * Major version of Rspack derived from an already-created compiler instance. + * Returns `null` for webpack compilers. + */ +export function getRspackMajorVersionFromCompiler( + compiler: RspackCompiler | WebpackCompiler +): number | null { + if ('rspackVersion' in compiler.webpack && compiler.webpack.rspackVersion) { + return Number(compiler.webpack.rspackVersion.split('.')[0]); + } + return null; +} From a11dd350a5779620c0edfafdf40053356dd36b54 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Mon, 6 Jul 2026 20:23:48 +0100 Subject: [PATCH 2/2] test: add coverage for rspack version detection helpers --- .../helpers/__tests__/rspackVersion.test.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 packages/repack/src/helpers/__tests__/rspackVersion.test.ts diff --git a/packages/repack/src/helpers/__tests__/rspackVersion.test.ts b/packages/repack/src/helpers/__tests__/rspackVersion.test.ts new file mode 100644 index 000000000..672eecae8 --- /dev/null +++ b/packages/repack/src/helpers/__tests__/rspackVersion.test.ts @@ -0,0 +1,134 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import type { Compiler as RspackCompiler } from '@rspack/core'; +import type { Compiler as WebpackCompiler } from 'webpack'; +import { + getRspackMajorVersion, + getRspackMajorVersionFromCompiler, + getRspackVersion, + isRspack2, +} from '../rspackVersion.js'; + +/** + * Creates a fake project directory containing `node_modules/@rspack/core` + * with the given version. The fake package's entrypoint throws, so any + * codepath that imports `@rspack/core` (instead of only resolving its + * `package.json`) fails loudly. + */ +function createFixtureProject(root: string, version: string): string { + const projectDir = fs.mkdtempSync(path.join(root, 'project-')); + const packageDir = path.join(projectDir, 'node_modules', '@rspack', 'core'); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync( + path.join(packageDir, 'package.json'), + JSON.stringify({ name: '@rspack/core', version, main: 'index.js' }) + ); + fs.writeFileSync( + path.join(packageDir, 'index.js'), + 'throw new Error("@rspack/core must not be imported by version detection");' + ); + return projectDir; +} + +describe('rspackVersion helpers', () => { + let tmpRoot: string; + let rspack1Project: string; + let rspack2Project: string; + let brokenProject: string; + + beforeAll(() => { + tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'repack-rspack-version-')); + rspack1Project = createFixtureProject(tmpRoot, '1.7.0'); + rspack2Project = createFixtureProject(tmpRoot, '2.3.4'); + // simulates an unusable @rspack/core install: package.json resolves but + // cannot be loaded, which exercises the same `catch -> null` branch as a + // missing install (jest's `require.resolve` treats `paths` as additional + // lookup paths and falls back to the workspace install, so a truly absent + // package cannot be simulated in-process here) + brokenProject = createFixtureProject(tmpRoot, '0.0.0'); + fs.writeFileSync( + path.join( + brokenProject, + 'node_modules', + '@rspack', + 'core', + 'package.json' + ), + 'not-json' + ); + }); + + afterAll(() => { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + }); + + describe('getRspackVersion', () => { + it('resolves the installed @rspack/core version by default', () => { + const { + version: installedVersion, + } = require('@rspack/core/package.json'); + expect(getRspackVersion()).toBe(installedVersion); + }); + + it('resolves @rspack/core from the given project context', () => { + expect(getRspackVersion(rspack2Project)).toBe('2.3.4'); + expect(getRspackVersion(rspack1Project)).toBe('1.7.0'); + }); + + it('does not import @rspack/core while detecting the version', () => { + // the fixture package's entrypoint throws on import, so a non-null + // result proves only package.json was read + expect(getRspackVersion(rspack2Project)).toBe('2.3.4'); + }); + + it('returns null when the version cannot be read', () => { + expect(getRspackVersion(brokenProject)).toBeNull(); + }); + }); + + describe('getRspackMajorVersion', () => { + it('parses the major version', () => { + expect(getRspackMajorVersion(rspack1Project)).toBe(1); + expect(getRspackMajorVersion(rspack2Project)).toBe(2); + }); + + it('returns null when the version cannot be read', () => { + expect(getRspackMajorVersion(brokenProject)).toBeNull(); + }); + }); + + describe('isRspack2', () => { + it('detects Rspack 2 from the given project context', () => { + expect(isRspack2(rspack2Project)).toBe(true); + expect(isRspack2(rspack1Project)).toBe(false); + }); + + it('returns false when the version cannot be read', () => { + expect(isRspack2(brokenProject)).toBe(false); + }); + }); + + describe('getRspackMajorVersionFromCompiler', () => { + it('reads the major version from an rspack compiler', () => { + // minimal mock: only `webpack.rspackVersion` is accessed + const compiler = { + webpack: { rspackVersion: '2.1.2' }, + } as unknown as RspackCompiler; + expect(getRspackMajorVersionFromCompiler(compiler)).toBe(2); + + const rspack1Compiler = { + webpack: { rspackVersion: '1.7.12' }, + } as unknown as RspackCompiler; + expect(getRspackMajorVersionFromCompiler(rspack1Compiler)).toBe(1); + }); + + it('returns null for a webpack compiler', () => { + // minimal mock: webpack compilers expose `version`, not `rspackVersion` + const compiler = { + webpack: { version: '5.99.0' }, + } as unknown as WebpackCompiler; + expect(getRspackMajorVersionFromCompiler(compiler)).toBeNull(); + }); + }); +});