Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/repack/src/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export type RemoveRecord<T> = T extends infer U & Record<string, any>

type ConfigKeys =
| 'name'
| 'cache'
| 'context'
| 'experiments'
| 'mode'
| 'devServer'
| 'entry'
Expand Down
134 changes: 134 additions & 0 deletions packages/repack/src/helpers/__tests__/rspackVersion.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

codex review: This still only covers the catch -> null branch through invalid package metadata, not the real optional-peer-absent path where require.resolve('@rspack/core/package.json', { paths: [project] }) throws MODULE_NOT_FOUND. Since optional-peer safety is part of this helper's contract, consider adding a child-process/raw Node test from a temp project with no @rspack/core installed; that avoids Jest's resolver fallback while locking down the actual absence case.

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();
});
});
});
1 change: 1 addition & 0 deletions packages/repack/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './errors.js';
export * from './helpers.js';
export * from './rspackVersion.js';
61 changes: 61 additions & 0 deletions packages/repack/src/helpers/rspackVersion.ts
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
dannyhw marked this conversation as resolved.
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;
}
Loading