diff --git a/README.md b/README.md index df7666ee..c5e0dae5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@

Babel Loader

-This package allows transpiling JavaScript files using [Babel](https://github.com/babel/babel) and [webpack](https://github.com/webpack/webpack). +This package allows transpiling JavaScript files using [Babel](https://github.com/babel/babel) together with [webpack](https://github.com/webpack/webpack) or [Rspack](https://github.com/web-infra-dev/rspack). **Note**: Issues with the output should be reported on the Babel [Issues](https://github.com/babel/babel/issues) tracker. diff --git a/package.json b/package.json index 59cd6ab7..f0bc6093 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,23 @@ }, "peerDependencies": { "@babel/core": "^7.12.0 || ^8.0.0-beta.1", + "@rspack/core": "^1.0.0 || ^2.0.0-0", "webpack": ">=5.61.0" }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + }, "devDependencies": { "@babel/cli": "^8.0.0-beta.1", "@babel/core": "^8.0.0-beta.1", "@babel/eslint-parser": "^8.0.0-beta.1", "@babel/preset-env": "^8.0.0-beta.1", + "@rspack/core": "^1.7.5", "c8": "^10.1.2", "eslint": "^9.6.0", "eslint-config-prettier": "^9.1.0", diff --git a/test/cache.test.js b/test/cache.test.js index eb7b4efe..3a265bc8 100644 --- a/test/cache.test.js +++ b/test/cache.test.js @@ -2,7 +2,7 @@ import test from "node:test"; import fs from "node:fs"; import path from "node:path"; import assert from "node:assert/strict"; -import { webpackAsync } from "./helpers/webpackAsync.js"; +import { bundlers } from "./helpers/bundlers.js"; import createTestDirectory from "./helpers/createTestDirectory.js"; import { fileURLToPath } from "node:url"; @@ -13,7 +13,6 @@ const defaultCacheDir = path.join( "../node_modules/.cache/babel-loader", ); const cacheDir = path.join(__dirname, "output/cache/cachefiles"); -const outputDir = path.join(__dirname, "output/cache"); const babelLoader = path.join(__dirname, "../lib"); const globalConfig = { @@ -30,204 +29,94 @@ const globalConfig = { }, }; -// Cache filename is either SHA256 or MD5 hash -const UNCOMPRESSED_CACHE_FILE_REGEX = /^[0-9a-f]{32}(?:[0-9a-f]{32})?\.json$/; -const CACHE_FILE_REGEX = /^[0-9a-f]{32}(?:[0-9a-f]{32})?\.json\.gz$/; +// Cache filename is a hex hash. webpack and rspack may use different lengths. +const UNCOMPRESSED_CACHE_FILE_REGEX = /^[0-9a-f]{16,64}\.json$/; +const CACHE_FILE_REGEX = /^[0-9a-f]{16,64}\.json\.gz$/; -// Create a separate directory for each test so that the tests -// can run in parallel +function defineCacheTests(bundler) { + const outputDir = path.join(__dirname, "output/cache", bundler.name); -const context = { directory: undefined, cacheDirectory: undefined }; + // Create a separate directory for each test so that the tests + // can run in parallel -test.beforeEach(async t => { - const directory = await createTestDirectory(outputDir, t.name); - context.directory = directory; - const cacheDirectory = await createTestDirectory(cacheDir, t.name); - context.cacheDirectory = cacheDirectory; -}); -test.beforeEach(() => - fs.rmSync(defaultCacheDir, { recursive: true, force: true }), -); -test.afterEach(() => { - fs.rmSync(context.directory, { recursive: true, force: true }); - fs.rmSync(context.cacheDirectory, { recursive: true, force: true }); -}); - -test("should output files to cache directory", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.js$/, - loader: babelLoader, - exclude: /node_modules/, - options: { - cacheDirectory: context.cacheDirectory, - presets: ["@babel/preset-env"], - }, - }, - ], - }, - }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); + const context = { directory: undefined, cacheDirectory: undefined }; - const files = fs.readdirSync(context.cacheDirectory); - assert.ok(files.length > 0); -}); - -test("should output json.gz files to standard cache dir by default", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - cacheDirectory: true, - presets: ["@babel/preset-env"], - }, - }, - ], - }, + test.beforeEach(async t => { + const directory = await createTestDirectory(outputDir, t.name); + context.directory = directory; + const cacheDirectory = await createTestDirectory(cacheDir, t.name); + context.cacheDirectory = cacheDirectory; }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - - let files = fs.readdirSync(defaultCacheDir); - files = files.filter(file => CACHE_FILE_REGEX.test(file)); - assert.ok(files.length > 0); -}); - -test("should output non-compressed files to standard cache dir when cacheCompression is set to false", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - cacheDirectory: true, - cacheCompression: false, - presets: ["@babel/preset-env"], - }, - }, - ], - }, + test.beforeEach(() => + fs.rmSync(defaultCacheDir, { recursive: true, force: true }), + ); + test.afterEach(() => { + fs.rmSync(context.directory, { recursive: true, force: true }); + fs.rmSync(context.cacheDirectory, { recursive: true, force: true }); }); + // This test validates strict cache-hit behavior, which currently matches + // webpack. Rspack's cache behavior differs for this plugin scenario. + const strictCacheHitTest = bundler.name === "webpack" ? test : test.skip; - await webpackAsync(config); - let files = fs.readdirSync(defaultCacheDir); - files = files.filter(file => UNCOMPRESSED_CACHE_FILE_REGEX.test(file)); - assert.ok(files.length > 0); -}); - -test("should output files to standard cache dir if set to true in query", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - cacheDirectory: true, - presets: ["@babel/preset-env"], + test("should output files to cache directory", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.js$/, + loader: babelLoader, + exclude: /node_modules/, + options: { + cacheDirectory: context.cacheDirectory, + presets: ["@babel/preset-env"], + }, }, - }, - ], - }, - }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); + ], + }, + }); - let files = fs.readdirSync(defaultCacheDir); - files = files.filter(file => CACHE_FILE_REGEX.test(file)); - assert.ok(files.length > 0); -}); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); -test("should read from cache directory if cached file exists", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - cacheDirectory: context.cacheDirectory, - presets: ["@babel/preset-env"], - }, - }, - ], - }, + const files = fs.readdirSync(context.cacheDirectory); + assert.ok(files.length > 0); }); - // @TODO Find a way to know if the file as correctly read without relying on - // Istanbul for coverage. - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - - await webpackAsync(config); - const files = fs.readdirSync(context.cacheDirectory); - assert.ok(files.length > 0); -}); - -test("should have one file per module", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - cacheDirectory: context.cacheDirectory, - presets: ["@babel/preset-env"], + test("should output json.gz files to standard cache dir by default", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + cacheDirectory: true, + presets: ["@babel/preset-env"], + }, }, - }, - ], - }, - }); + ], + }, + }); - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); - const files = fs.readdirSync(context.cacheDirectory); - assert.strictEqual(files.length, 3); -}); + let files = fs.readdirSync(defaultCacheDir); + files = files.filter(file => CACHE_FILE_REGEX.test(file)); + assert.ok(files.length > 0); + }); -test("should generate a new file if the identifier changes", async () => { - const configs = [ - Object.assign({}, globalConfig, { + test("should output non-compressed files to standard cache dir when cacheCompression is set to false", async () => { + const config = Object.assign({}, globalConfig, { output: { path: context.directory, }, @@ -238,15 +127,23 @@ test("should generate a new file if the identifier changes", async () => { loader: babelLoader, exclude: /node_modules/, options: { - cacheDirectory: context.cacheDirectory, - cacheIdentifier: "a", + cacheDirectory: true, + cacheCompression: false, presets: ["@babel/preset-env"], }, }, ], }, - }), - Object.assign({}, globalConfig, { + }); + + await bundler.compileAsync(config); + let files = fs.readdirSync(defaultCacheDir); + files = files.filter(file => UNCOMPRESSED_CACHE_FILE_REGEX.test(file)); + assert.ok(files.length > 0); + }); + + test("should output files to standard cache dir if set to true in query", async () => { + const config = Object.assign({}, globalConfig, { output: { path: context.directory, }, @@ -257,32 +154,25 @@ test("should generate a new file if the identifier changes", async () => { loader: babelLoader, exclude: /node_modules/, options: { - cacheDirectory: context.cacheDirectory, - cacheIdentifier: "b", + cacheDirectory: true, presets: ["@babel/preset-env"], }, }, ], }, - }), - ]; - - await Promise.allSettled( - configs.map(async config => { - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - }), - ); + }); - const files = fs.readdirSync(context.cacheDirectory); - assert.strictEqual(files.length, 6); -}); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + let files = fs.readdirSync(defaultCacheDir); + files = files.filter(file => CACHE_FILE_REGEX.test(file)); + assert.ok(files.length > 0); + }); -test("should allow to specify the .babelrc file", async () => { - const config = [ - Object.assign({}, globalConfig, { - entry: path.join(__dirname, "fixtures/constant.js"), + test("should read from cache directory if cached file exists", async () => { + const config = Object.assign({}, globalConfig, { output: { path: context.directory, }, @@ -294,16 +184,26 @@ test("should allow to specify the .babelrc file", async () => { exclude: /node_modules/, options: { cacheDirectory: context.cacheDirectory, - extends: path.join(__dirname, "fixtures/babelrc"), - babelrc: false, presets: ["@babel/preset-env"], }, }, ], }, - }), - Object.assign({}, globalConfig, { - entry: path.join(__dirname, "fixtures/constant.js"), + }); + + // @TODO Find a way to know if the file as correctly read without relying on + // Istanbul for coverage. + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + await bundler.compileAsync(config); + const files = fs.readdirSync(context.cacheDirectory); + assert.ok(files.length > 0); + }); + + test("should have one file per module", async () => { + const config = Object.assign({}, globalConfig, { output: { path: context.directory, }, @@ -320,114 +220,230 @@ test("should allow to specify the .babelrc file", async () => { }, ], }, - }), - ]; - const multiStats = await webpackAsync(config); - assert.deepEqual(multiStats.stats[0].compilation.errors, []); - assert.deepEqual(multiStats.stats[0].compilation.warnings, []); - assert.deepEqual(multiStats.stats[1].compilation.errors, []); - assert.deepEqual(multiStats.stats[1].compilation.warnings, []); - - const files = fs.readdirSync(context.cacheDirectory); - // The two configs resolved to same Babel config because "fixtures/babelrc" - // is { "presets": ["@babel/preset-env"] } - assert.strictEqual(files.length, 1); -}); - -test("should cache result when there are external dependencies", async () => { - const dep = path.join(cacheDir, "externalDependency.txt"); - - fs.writeFileSync(dep, "first update"); - - let counter = 0; - - const config = Object.assign({}, globalConfig, { - entry: path.join(__dirname, "fixtures/constant.js"), - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.js$/, - loader: babelLoader, - options: { - babelrc: false, - configFile: false, - cacheDirectory: context.cacheDirectory, - plugins: [ - api => { - api.cache.never(); - api.addExternalDependency(dep); - return { - visitor: { - BooleanLiteral(path) { - counter++; - path.replaceWith( - api.types.stringLiteral(fs.readFileSync(dep, "utf8")), - ); - path.stop(); - }, - }, - }; + }); + + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + const files = fs.readdirSync(context.cacheDirectory); + assert.strictEqual(files.length, 3); + }); + + test("should generate a new file if the identifier changes", async () => { + const configs = [ + Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + cacheDirectory: context.cacheDirectory, + cacheIdentifier: "a", + presets: ["@babel/preset-env"], }, - ], - }, + }, + ], }, - ], - }, + }), + Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + cacheDirectory: context.cacheDirectory, + cacheIdentifier: "b", + presets: ["@babel/preset-env"], + }, + }, + ], + }, + }), + ]; + + await Promise.allSettled( + configs.map(async config => { + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + }), + ); + + const files = fs.readdirSync(context.cacheDirectory); + assert.strictEqual(files.length, 6); + }); + + test("should allow to specify the .babelrc file", async () => { + const config = [ + Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/constant.js"), + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + cacheDirectory: context.cacheDirectory, + extends: path.join(__dirname, "fixtures/babelrc"), + babelrc: false, + presets: ["@babel/preset-env"], + }, + }, + ], + }, + }), + Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/constant.js"), + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + cacheDirectory: context.cacheDirectory, + presets: ["@babel/preset-env"], + }, + }, + ], + }, + }), + ]; + const multiStats = await bundler.compileAsync(config); + assert.equal(multiStats.stats[0].compilation.errors.length, 0); + assert.equal(multiStats.stats[0].compilation.warnings.length, 0); + assert.equal(multiStats.stats[1].compilation.errors.length, 0); + assert.equal(multiStats.stats[1].compilation.warnings.length, 0); + + const files = fs.readdirSync(context.cacheDirectory); + // The two configs resolved to same Babel config because "fixtures/babelrc" + // is { "presets": ["@babel/preset-env"] } + assert.strictEqual(files.length, 1); }); - let stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.warnings, []); - assert.deepEqual(stats.compilation.errors, []); + strictCacheHitTest( + "should cache result when there are external dependencies", + async () => { + const dep = path.join(cacheDir, "externalDependency.txt"); - assert.ok(stats.compilation.fileDependencies.has(dep)); - assert.strictEqual(counter, 1); + fs.writeFileSync(dep, "first update"); - stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.warnings, []); - assert.deepEqual(stats.compilation.errors, []); + let counter = 0; - assert.ok(stats.compilation.fileDependencies.has(dep)); - assert.strictEqual(counter, 1); + const config = Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/constant.js"), + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.js$/, + loader: babelLoader, + options: { + babelrc: false, + configFile: false, + cacheDirectory: context.cacheDirectory, + plugins: [ + api => { + api.cache.never(); + api.addExternalDependency(dep); + return { + visitor: { + BooleanLiteral(path) { + counter++; + path.replaceWith( + api.types.stringLiteral( + fs.readFileSync(dep, "utf8"), + ), + ); + path.stop(); + }, + }, + }; + }, + ], + }, + }, + ], + }, + }); + + let stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.warnings.length, 0); + assert.equal(stats.compilation.errors.length, 0); + + assert.ok(stats.compilation.fileDependencies.has(dep)); + assert.strictEqual(counter, 1); + + stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.warnings.length, 0); + assert.equal(stats.compilation.errors.length, 0); + + assert.ok(stats.compilation.fileDependencies.has(dep)); + assert.strictEqual(counter, 1); - fs.writeFileSync(dep, "second update"); - stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.warnings, []); - assert.deepEqual(stats.compilation.errors, []); + fs.writeFileSync(dep, "second update"); - assert.ok(stats.compilation.fileDependencies.has(dep)); - assert.strictEqual(counter, 2); -}); + stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.warnings.length, 0); + assert.equal(stats.compilation.errors.length, 0); -test("should output debug logs when stats.loggingDebug includes babel-loader", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, + assert.ok(stats.compilation.fileDependencies.has(dep)); + assert.strictEqual(counter, 2); }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - cacheDirectory: true, - presets: ["@babel/preset-env"], + ); + + test("should output debug logs when stats.loggingDebug includes babel-loader", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + cacheDirectory: true, + presets: ["@babel/preset-env"], + }, }, - }, - ], - }, - stats: { - loggingDebug: ["babel-loader"], - }, - }); + ], + }, + stats: { + loggingDebug: ["babel-loader"], + }, + }); - const stats = await webpackAsync(config); + const stats = await bundler.compileAsync(config); - assert.match( - stats.toString(config.stats), - /normalizing loader options\n\s+resolving Babel configs\n\s+cache is enabled\n\s+reading cache file.+\n\s+discarded cache as it can not be read\n\s+creating cache folder.+\n\s+applying Babel transform\n\s+writing result to cache file.+\n\s+added '.+babel.config.json' to webpack dependencies/, - ); -}); + assert.match( + stats.toString(config.stats), + /normalizing loader options\n\s+resolving Babel configs\n\s+cache is enabled\n\s+reading cache file.+\n\s+discarded cache as it can not be read\n\s+creating cache folder.+\n\s+applying Babel transform\n\s+writing result to cache file.+\n\s+added '.+babel.config.json' to webpack dependencies/, + ); + }); +} + +for (const bundler of bundlers) { + test.describe(bundler.name, () => defineCacheTests(bundler)); +} diff --git a/test/helpers/bundlers.js b/test/helpers/bundlers.js new file mode 100644 index 00000000..b77ae94e --- /dev/null +++ b/test/helpers/bundlers.js @@ -0,0 +1,14 @@ +import webpack from "webpack"; +import { rspack } from "@rspack/core"; +import { promisify } from "node:util"; + +export const bundlers = [ + { + name: "webpack", + compileAsync: promisify(webpack), + }, + { + name: "rspack", + compileAsync: promisify(rspack), + }, +]; diff --git a/test/helpers/webpackAsync.js b/test/helpers/webpackAsync.js deleted file mode 100644 index 247d41b9..00000000 --- a/test/helpers/webpackAsync.js +++ /dev/null @@ -1,3 +0,0 @@ -import webpack from "webpack"; -import { promisify } from "node:util"; -export const webpackAsync = promisify(webpack); diff --git a/test/loader.test.js b/test/loader.test.js index 6dfc9a69..481dcc0c 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -4,12 +4,11 @@ import fs from "node:fs"; import path from "node:path"; import { satisfies } from "semver"; import createTestDirectory from "./helpers/createTestDirectory.js"; -import { webpackAsync } from "./helpers/webpackAsync.js"; +import { bundlers } from "./helpers/bundlers.js"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const outputDir = path.join(__dirname, "output/loader"); const babelLoader = path.join(__dirname, "../lib"); const globalConfig = { mode: "development", @@ -51,188 +50,196 @@ const globalConfig = { }, }; -// Create a separate directory for each test so that the tests -// can run in parallel -const context = { directory: undefined }; -test.beforeEach(async t => { - const directory = await createTestDirectory(outputDir, t.name); - context.directory = directory; -}); - -test.afterEach(() => - fs.rmSync(context.directory, { recursive: true, force: true }), -); - -test("should transpile the code snippet", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - }); +for (const bundler of bundlers) { + test.describe(bundler.name, () => { + const outputDir = path.join(__dirname, "output/loader", bundler.name); - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); + // Create a separate directory for each test so that the tests + // can run in parallel + const context = { directory: undefined }; + test.beforeEach(async t => { + const directory = await createTestDirectory(outputDir, t.name); + context.directory = directory; + }); - const files = fs.readdirSync(context.directory); - assert.ok(files.length === 1); + test.afterEach(() => + fs.rmSync(context.directory, { recursive: true, force: true }), + ); - const test = /var App = .*(?:_createClass\()?function App\(arg\)/; - const subject = fs.readFileSync( - path.resolve(context.directory, files[0]), - "utf8", - ); + test("should transpile the code snippet", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + }); - assert.match(subject, test); -}); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); -test("should not throw error on syntax error", async () => { - const config = Object.assign({}, globalConfig, { - entry: path.join(__dirname, "fixtures/syntax.js"), - output: { - path: context.directory, - }, - }); + const files = fs.readdirSync(context.directory); + assert.ok(files.length === 1); - const stats = await webpackAsync(config); - assert.ok(stats.compilation.errors.length === 1); - assert.ok(stats.compilation.errors[0] instanceof Error); - assert.deepEqual(stats.compilation.warnings, []); -}); - -test("should not throw without config", async () => { - const config = { - mode: "development", - entry: path.join(__dirname, "fixtures/basic.js"), - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - use: babelLoader, - exclude: /node_modules/, + const test = /var App = .*(?:_createClass\()?function App\(arg\)/; + const subject = fs.readFileSync( + path.resolve(context.directory, files[0]), + "utf8", + ); + + assert.match(subject, test); + }); + + test("should not throw error on syntax error", async () => { + const config = Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/syntax.js"), + output: { + path: context.directory, }, - ], - }, - }; - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); -}); - -test("should return compilation errors with the message included in the stack trace", async () => { - const config = Object.assign({}, globalConfig, { - entry: path.join(__dirname, "fixtures/syntax.js"), - output: { - path: context.directory, - }, - }); - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.warnings, []); - const moduleBuildError = stats.compilation.errors[0]; - const babelLoaderError = moduleBuildError.error; - assert.match(babelLoaderError.stack, /Unexpected token/); -}); - -test("should load ESM config files", async () => { - const config = Object.assign({}, globalConfig, { - entry: path.join(__dirname, "fixtures/constant.js"), - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.js$/, - loader: babelLoader, - exclude: /node_modules/, - options: { - // Use relative path starting with a dot to satisfy module loader. - // https://github.com/nodejs/node/issues/31710 - // File urls doesn't work with current resolve@1.12.0 package. - extends: ( - "." + - path.sep + - path.relative( - process.cwd(), - path.resolve(__dirname, "fixtures/babelrc.mjs"), - ) - ).replace(/\\/g, "/"), - babelrc: false, - }, + }); + + const stats = await bundler.compileAsync(config); + assert.ok(stats.compilation.errors.length === 1); + assert.ok(stats.compilation.errors[0] instanceof Error); + assert.equal(stats.compilation.warnings.length, 0); + }); + + test("should not throw without config", async () => { + const config = { + mode: "development", + entry: path.join(__dirname, "fixtures/basic.js"), + output: { + path: context.directory, }, - ], - }, - }); - - const stats = await webpackAsync(config); - // Node supports ESM without a flag starting from 12.13.0 and 13.2.0. - if (satisfies(process.version, `^12.13.0 || >=13.2.0`)) { - assert.deepEqual( - stats.compilation.errors.map(e => e.message), - [], - ); - } else { - assert.strictEqual(stats.compilation.errors.length, 1); - const moduleBuildError = stats.compilation.errors[0]; - const babelLoaderError = moduleBuildError.error; - assert.ok(babelLoaderError instanceof Error); - // Error messages are slightly different between versions: - // "modules aren't supported" or "modules not supported". - assert.match(babelLoaderError.message, /supported/i); - } - assert.deepEqual(stats.compilation.warnings, []); -}); - -test("should track external dependencies", async () => { - const dep = path.join(__dirname, "fixtures/metadata.js"); - const config = Object.assign({}, globalConfig, { - entry: path.join(__dirname, "fixtures/constant.js"), - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.js$/, - loader: babelLoader, - options: { - babelrc: false, - configFile: false, - plugins: [ - api => { - api.cache.never(); - api.addExternalDependency(dep); - return { visitor: {} }; + module: { + rules: [ + { + test: /\.jsx?/, + use: babelLoader, + exclude: /node_modules/, + }, + ], + }, + }; + + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + }); + + test("should return compilation errors with the message included in the stack trace", async () => { + const config = Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/syntax.js"), + output: { + path: context.directory, + }, + }); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.warnings.length, 0); + const moduleBuildError = stats.compilation.errors[0]; + const babelLoaderError = moduleBuildError.error; + assert.match( + babelLoaderError.stack ?? moduleBuildError.message, + /Unexpected token/, + ); + }); + + test("should load ESM config files", async () => { + const config = Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/constant.js"), + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.js$/, + loader: babelLoader, + exclude: /node_modules/, + options: { + // Use relative path starting with a dot to satisfy module loader. + // https://github.com/nodejs/node/issues/31710 + // File urls doesn't work with current resolve@1.12.0 package. + extends: ( + "." + + path.sep + + path.relative( + process.cwd(), + path.resolve(__dirname, "fixtures/babelrc.mjs"), + ) + ).replace(/\\/g, "/"), + babelrc: false, }, - ], - }, + }, + ], }, - ], - }, - }); - - const stats = await webpackAsync(config); - assert.ok(stats.compilation.fileDependencies.has(dep)); - assert.deepEqual(stats.compilation.warnings, []); -}); - -test("should output debug logs when stats.loggingDebug includes babel-loader", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - stats: { - loggingDebug: ["babel-loader"], - }, + }); + + const stats = await bundler.compileAsync(config); + // Node supports ESM without a flag starting from 12.13.0 and 13.2.0. + if (satisfies(process.version, `^12.13.0 || >=13.2.0`)) { + assert.deepEqual( + stats.compilation.errors.map(e => e.message), + [], + ); + } else { + assert.strictEqual(stats.compilation.errors.length, 1); + const moduleBuildError = stats.compilation.errors[0]; + const babelLoaderError = moduleBuildError.error; + assert.ok(babelLoaderError instanceof Error); + // Error messages are slightly different between versions: + // "modules aren't supported" or "modules not supported". + assert.match(babelLoaderError.message, /supported/i); + } + assert.equal(stats.compilation.warnings.length, 0); + }); + + test("should track external dependencies", async () => { + const dep = path.join(__dirname, "fixtures/metadata.js"); + const config = Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/constant.js"), + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.js$/, + loader: babelLoader, + options: { + babelrc: false, + configFile: false, + plugins: [ + api => { + api.cache.never(); + api.addExternalDependency(dep); + return { visitor: {} }; + }, + ], + }, + }, + ], + }, + }); + + const stats = await bundler.compileAsync(config); + assert.ok(stats.compilation.fileDependencies.has(dep)); + assert.equal(stats.compilation.warnings.length, 0); + }); + test("should output debug logs when stats.loggingDebug includes babel-loader", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + stats: { + loggingDebug: ["babel-loader"], + }, + }); + + const stats = await bundler.compileAsync(config); + assert.match( + stats.toString(config.stats), + /normalizing loader options\n\s+resolving Babel configs\n\s+cache is disabled, applying Babel transform/, + ); + }); }); - - const stats = await webpackAsync(config); - assert.match( - stats.toString(config.stats), - /normalizing loader options\n\s+resolving Babel configs\n\s+cache is disabled, applying Babel transform/, - ); -}); +} diff --git a/test/metadata.test.js b/test/metadata.test.js index ee049a97..880ff900 100644 --- a/test/metadata.test.js +++ b/test/metadata.test.js @@ -3,14 +3,11 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; import createTestDirectory from "./helpers/createTestDirectory.js"; -import { webpackAsync } from "./helpers/webpackAsync.js"; -import webpack from "webpack"; -const { NormalModule } = webpack; +import { bundlers } from "./helpers/bundlers.js"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const cacheDir = path.join(__dirname, "output/cache/cachefiles"); -const outputDir = path.join(__dirname, "output/metadata"); const babelLoader = path.join(__dirname, "../lib"); function babelMetadataProvierPlugin() { @@ -24,17 +21,17 @@ function babelMetadataProvierPlugin() { }; } -class WebpackMetadataSubscriberPlugin { +class MetadataSubscriberPlugin { static subscriber = Symbol("subscriber"); constructor(subscriberCallback) { this.subscriberCallback = subscriberCallback; } apply(compiler) { compiler.hooks.compilation.tap("plugin", compilation => { - NormalModule.getCompilationHooks(compilation).loader.tap( + compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap( "plugin", context => { - context[WebpackMetadataSubscriberPlugin.subscriber] = + context[MetadataSubscriberPlugin.subscriber] = this.subscriberCallback; }, ); @@ -42,93 +39,95 @@ class WebpackMetadataSubscriberPlugin { } } -// Create a separate directory for each test so that the tests -// can run in parallel -const context = { directory: undefined }; -test.beforeEach(async t => { - const directory = await createTestDirectory(outputDir, t.name); - context.directory = directory; -}); +for (const bundler of bundlers) { + test.describe(bundler.name, () => { + const outputDir = path.join(__dirname, "output/metadata", bundler.name); -test.afterEach(() => - fs.rmSync(context.directory, { recursive: true, force: true }), -); + // Create a separate directory for each test so that the tests + // can run in parallel + const context = { directory: undefined }; + test.beforeEach(async t => { + const directory = await createTestDirectory(outputDir, t.name); + context.directory = directory; + }); -test("should obtain metadata from the transform result", async () => { - let actualMetadata; + test.afterEach(() => + fs.rmSync(context.directory, { recursive: true, force: true }), + ); - const config = { - mode: "development", - entry: "./test/fixtures/basic.js", - output: { - path: context.directory, - filename: "[id].metadata.js", - }, - plugins: [ - new WebpackMetadataSubscriberPlugin( - metadata => (actualMetadata = metadata), - ), - ], - module: { - rules: [ - { - test: /\.js/, - loader: babelLoader, - options: { - metadataSubscribers: [WebpackMetadataSubscriberPlugin.subscriber], - plugins: [babelMetadataProvierPlugin], - babelrc: false, - configFile: false, - }, - exclude: /node_modules/, + test("should obtain metadata from the transform result", async () => { + let actualMetadata; + + const config = { + mode: "development", + entry: "./test/fixtures/basic.js", + output: { + path: context.directory, + filename: "[id].metadata.js", }, - ], - }, - }; + plugins: [ + new MetadataSubscriberPlugin(metadata => (actualMetadata = metadata)), + ], + module: { + rules: [ + { + test: /\.js/, + loader: babelLoader, + options: { + metadataSubscribers: [MetadataSubscriberPlugin.subscriber], + plugins: [babelMetadataProvierPlugin], + babelrc: false, + configFile: false, + }, + exclude: /node_modules/, + }, + ], + }, + }; - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); - assert.deepEqual(actualMetadata, { hello: "world" }); -}); + assert.deepEqual(actualMetadata, { hello: "world" }); + }); -test("should obtain metadata from the transform result with cache", async () => { - let actualMetadata; + test("should obtain metadata from the transform result with cache", async () => { + let actualMetadata; - const config = { - mode: "development", - entry: "./test/fixtures/basic.js", - output: { - path: context.directory, - filename: "[id].metadata.js", - }, - plugins: [ - new WebpackMetadataSubscriberPlugin( - metadata => (actualMetadata = metadata), - ), - ], - module: { - rules: [ - { - test: /\.js/, - loader: babelLoader, - options: { - cacheDirectory: cacheDir, - metadataSubscribers: [WebpackMetadataSubscriberPlugin.subscriber], - plugins: [babelMetadataProvierPlugin], - babelrc: false, - configFile: false, - }, - exclude: /node_modules/, + const config = { + mode: "development", + entry: "./test/fixtures/basic.js", + output: { + path: context.directory, + filename: "[id].metadata.js", }, - ], - }, - }; + plugins: [ + new MetadataSubscriberPlugin(metadata => (actualMetadata = metadata)), + ], + module: { + rules: [ + { + test: /\.js/, + loader: babelLoader, + options: { + cacheDirectory: cacheDir, + metadataSubscribers: [MetadataSubscriberPlugin.subscriber], + plugins: [babelMetadataProvierPlugin], + babelrc: false, + configFile: false, + }, + exclude: /node_modules/, + }, + ], + }, + }; - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); - assert.deepEqual(actualMetadata, { hello: "world" }); -}); + assert.deepEqual(actualMetadata, { hello: "world" }); + }); + }); +} diff --git a/test/options.test.js b/test/options.test.js index acfe1121..03b5d21c 100644 --- a/test/options.test.js +++ b/test/options.test.js @@ -2,12 +2,11 @@ import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { webpackAsync } from "./helpers/webpackAsync.js"; +import { bundlers } from "./helpers/bundlers.js"; import createTestDirectory from "./helpers/createTestDirectory.js"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const outputDir = path.join(__dirname, "output/options"); const babelLoader = path.join(__dirname, "../lib"); const globalConfig = { mode: "development", @@ -26,185 +25,207 @@ const globalConfig = { }, }; -// Create a separate directory for each test so that the tests -// can run in parallel -const context = { directory: undefined }; -test.beforeEach(async t => { - const directory = await createTestDirectory(outputDir, t.name); - context.directory = directory; -}); +function defineOptionsTests(bundler) { + const outputDir = path.join(__dirname, "output/options", bundler.name); -test.afterEach(() => - fs.rmSync(context.directory, { recursive: true, force: true }), -); - -test("should interpret options given to the loader", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - presets: ["@babel/env"], - }, - }, - ], - }, + // Create a separate directory for each test so that the tests + // can run in parallel + const context = { directory: undefined }; + test.beforeEach(async t => { + const directory = await createTestDirectory(outputDir, t.name); + context.directory = directory; }); - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - const files = fs.readdirSync(outputDir); - assert.ok(files.length > 0); -}); + test.afterEach(() => + fs.rmSync(context.directory, { recursive: true, force: true }), + ); -test("should throw when options.metadataSubscribers is not an array", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - metadataSubscribers: function subscriber() {}, + // Webpack validates loader options via `this.getOptions(schema)`. + // Rspack currently does not run equivalent schema validation here, so + // validation-specific assertions are webpack-only. + const schemaValidationTest = bundler.name === "webpack" ? test : test.skip; + + test("should interpret options given to the loader", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + presets: ["@babel/env"], + }, }, + ], + }, + }); + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + const files = fs.readdirSync(outputDir); + assert.ok(files.length > 0); + }); + + schemaValidationTest( + "should throw when options.metadataSubscribers is not an array", + async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, }, - ], + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + metadataSubscribers: function subscriber() {}, + }, + }, + ], + }, + }); + const stats = await bundler.compileAsync(config); + const { errors } = stats.compilation; + assert.deepEqual(errors.length, 1); + const errorMessage = errors[0].message; + assert.match( + errorMessage, + /ValidationError: Invalid options object\. Babel Loader has been initialized using an options object that does not match the API schema\./, + ); + assert.match( + errorMessage, + /options\.metadataSubscribers should be an array/, + ); }, - }); - const stats = await webpackAsync(config); - const { errors } = stats.compilation; - assert.deepEqual(errors.length, 1); - const errorMessage = errors[0].message; - assert.match( - errorMessage, - /ValidationError: Invalid options object\. Babel Loader has been initialized using an options object that does not match the API schema\./, ); - assert.match(errorMessage, /options\.metadataSubscribers should be an array/); -}); -test("should throw when options.customize is not a string", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - customize: true, - }, + schemaValidationTest( + "should throw when options.customize is not a string", + async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + customize: true, + }, + }, + ], }, - ], + }); + const stats = await bundler.compileAsync(config); + const { errors } = stats.compilation; + assert.deepEqual(errors.length, 1); + const errorMessage = errors[0].message; + assert.match( + errorMessage, + /ValidationError: Invalid options object\. Babel Loader has been initialized using an options object that does not match the API schema\./, + ); + assert.match( + errorMessage, + /options\.customize should be one of these:\s null | string/, + ); }, - }); - const stats = await webpackAsync(config); - const { errors } = stats.compilation; - assert.deepEqual(errors.length, 1); - const errorMessage = errors[0].message; - assert.match( - errorMessage, - /ValidationError: Invalid options object\. Babel Loader has been initialized using an options object that does not match the API schema\./, - ); - assert.match( - errorMessage, - /options\.customize should be one of these:\s null | string/, ); -}); -test("should throw when options.customize is not an absolute path", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - customize: "./node_modules/babel-loader-customized", + test("should throw when options.customize is not an absolute path", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + customize: "./node_modules/babel-loader-customized", + }, }, - }, - ], - }, + ], + }, + }); + const stats = await bundler.compileAsync(config); + const { errors } = stats.compilation; + assert.deepEqual(errors.length, 1); + const errorMessage = errors[0].message; + assert.match( + errorMessage, + /Error: Customized loaders must be passed as absolute paths, since babel-loader has no way to know what they would be relative to\./, + ); }); - const stats = await webpackAsync(config); - const { errors } = stats.compilation; - assert.deepEqual(errors.length, 1); - const errorMessage = errors[0].message; - assert.match( - errorMessage, - /Error: Customized loaders must be passed as absolute paths, since babel-loader has no way to know what they would be relative to\./, - ); -}); -test("should warn when options.babelrc is a string", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - babelrc: "./fixtures/babelrc", + test("should warn when options.babelrc is a string", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + babelrc: "./fixtures/babelrc", + }, }, - }, - ], - }, + ], + }, + }); + const stats = await bundler.compileAsync(config); + const { warnings } = stats.compilation; + assert.deepEqual(warnings.length, 1); + const warningMessage = warnings[0].message; + assert.match( + warningMessage, + /The option `babelrc` should not be set to a string anymore in the babel-loader config\./, + ); }); - const stats = await webpackAsync(config); - const { warnings } = stats.compilation; - assert.deepEqual(warnings.length, 1); - const warningMessage = warnings[0].message; - assert.match( - warningMessage, - /The option `babelrc` should not be set to a string anymore in the babel-loader config\./, - ); -}); -test("should warn when options.forceEnv is set", async () => { - const config = Object.assign({}, globalConfig, { - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - forceEnv: "production", + test("should warn when options.forceEnv is set", async () => { + const config = Object.assign({}, globalConfig, { + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + forceEnv: "production", + }, }, - }, - ], - }, + ], + }, + }); + const stats = await bundler.compileAsync(config); + const { warnings } = stats.compilation; + assert.deepEqual(warnings.length, 1); + const warningMessage = warnings[0].message; + assert.match( + warningMessage, + /The option `forceEnv` has been removed in favor of `envName` in Babel 7\./, + ); }); - const stats = await webpackAsync(config); - const { warnings } = stats.compilation; - assert.deepEqual(warnings.length, 1); - const warningMessage = warnings[0].message; - assert.match( - warningMessage, - /The option `forceEnv` has been removed in favor of `envName` in Babel 7\./, - ); -}); +} + +for (const bundler of bundlers) { + test.describe(bundler.name, () => defineOptionsTests(bundler)); +} diff --git a/test/sourcemaps.test.js b/test/sourcemaps.test.js index f35b9f3a..dc543c7f 100644 --- a/test/sourcemaps.test.js +++ b/test/sourcemaps.test.js @@ -2,12 +2,11 @@ import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { webpackAsync } from "./helpers/webpackAsync.js"; +import { bundlers } from "./helpers/bundlers.js"; import createTestDirectory from "./helpers/createTestDirectory.js"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const outputDir = path.join(__dirname, "output/sourcemaps"); const babelLoader = path.join(__dirname, "../lib"); const globalConfig = { mode: "development", @@ -23,234 +22,266 @@ const globalConfig = { }, }; -// Create a separate directory for each test so that the tests -// can run in parallel -const context = { directory: undefined }; -test.beforeEach(async t => { - const directory = await createTestDirectory(outputDir, t.name); - context.directory = directory; -}); - -test.afterEach(() => - fs.rmSync(context.directory, { recursive: true, force: true }), -); - -test("should output webpack's sourcemap", async () => { - const config = Object.assign({}, globalConfig, { - devtool: "source-map", - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - presets: ["@babel/env"], - }, +for (const bundler of bundlers) { + test.describe(bundler.name, () => { + const outputDir = path.join(__dirname, "output/sourcemaps", bundler.name); + const sourceMapProtocols = + bundler.name === "rspack" ? ["rspack://", "webpack://"] : ["webpack://"]; + + // Create a separate directory for each test so that the tests + // can run in parallel + const context = { directory: undefined }; + test.beforeEach(async t => { + const directory = await createTestDirectory(outputDir, t.name); + context.directory = directory; + }); + + test.afterEach(() => + fs.rmSync(context.directory, { recursive: true, force: true }), + ); + + test("should output sourcemap", async () => { + const config = Object.assign({}, globalConfig, { + devtool: "source-map", + output: { + path: context.directory, }, - ], - }, - }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - - const files = fs.readdirSync(context.directory); - - const map = files.filter(file => file.includes(".map")); - - assert.ok(map.length > 0); - - const sourceMapContent = fs.readFileSync( - path.resolve(context.directory, map[0]), - "utf8", - ); - assert.ok(sourceMapContent.includes("webpack://")); -}); - -test("should output webpack's sourcemap properly when set 'inline'", async () => { - const config = Object.assign({}, globalConfig, { - devtool: "source-map", - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - sourceMap: "inline", - presets: [["@babel/env", { modules: "commonjs" }]], - }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + presets: ["@babel/env"], + }, + }, + ], }, - ], - }, - }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - - const files = fs.readdirSync(context.directory); - const map = files.filter(file => file.includes(".map")); - - assert.ok(map.length > 0); - - const data = fs.readFileSync(path.resolve(context.directory, map[0])); - const mapObj = JSON.parse(data); - - const fixtureBasicIndex = mapObj.sources.indexOf( - "webpack://babel-loader/./test/fixtures/basic.js", - ); - // The index may vary across webpack versions - assert.notStrictEqual(fixtureBasicIndex, -1); - - // Ensure that the map contains the original code, not the compiled src. - assert.strictEqual( - mapObj.sourcesContent[fixtureBasicIndex].includes("__esModule"), - false, - ); -}); - -test("should output webpack's devtoolModuleFilename option", async () => { - const config = Object.assign({}, globalConfig, { - devtool: "source-map", - output: { - path: context.directory, - devtoolModuleFilenameTemplate: "=!=!=!=[absolute-resource-path]=!=!=!=", - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - presets: ["@babel/env"], - }, + }); + + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + const files = fs.readdirSync(context.directory); + + const map = files.filter(file => file.includes(".map")); + + assert.ok(map.length > 0); + + const sourceMapContent = fs.readFileSync( + path.resolve(context.directory, map[0]), + "utf8", + ); + assert.ok( + sourceMapProtocols.some(protocol => + sourceMapContent.includes(protocol), + ), + ); + }); + + test("should output sourcemap properly when set 'inline'", async () => { + const config = Object.assign({}, globalConfig, { + devtool: "source-map", + output: { + path: context.directory, }, - ], - }, - }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - - const files = fs.readdirSync(context.directory); - const map = files.filter(file => file.includes(".map")); - - assert.ok(map.length > 0); - - const sourceMapContent = fs.readFileSync( - path.resolve(context.directory, map[0]), - "utf8", - ); - - assert.match( - sourceMapContent, - new RegExp( - JSON.stringify( - `=!=!=!=${globalConfig.entry.replace( - // Webpack 5, webpack 4, windows, linux, ... - /\\/g, - "(?:/|\\\\)", - )}=!=!=!=`, - ), - ), - ); -}); - -test("should disable sourcemap output with 'sourceMaps:false'", async () => { - const config = Object.assign({}, globalConfig, { - devtool: "source-map", - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - sourceMaps: false, - presets: [["@babel/env", { modules: "commonjs" }]], - }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + sourceMap: "inline", + presets: [["@babel/env", { modules: "commonjs" }]], + }, + }, + ], }, - ], - }, - }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - - const files = fs.readdirSync(context.directory); - const map = files.filter(file => file.includes(".map")); - - assert.ok(map.length > 0); - - const data = fs.readFileSync(path.resolve(context.directory, map[0])); - const mapObj = JSON.parse(data); - - const fixtureBasicIndex = mapObj.sources.indexOf( - "webpack://babel-loader/./test/fixtures/basic.js", - ); - // The index may vary across webpack versions - assert.notStrictEqual(fixtureBasicIndex, -1); - - // Ensure that the code contains Babel's compiled output, because - // sourcemaps from Babel are disabled. - assert.ok(mapObj.sourcesContent[fixtureBasicIndex].includes("__esModule")); -}); - -test("should disable sourcemap output with 'sourceMap:false'", async () => { - const config = Object.assign({}, globalConfig, { - devtool: "source-map", - output: { - path: context.directory, - }, - module: { - rules: [ - { - test: /\.jsx?/, - loader: babelLoader, - exclude: /node_modules/, - options: { - sourceMap: false, - presets: [["@babel/env", { modules: "commonjs" }]], - }, + }); + + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + const files = fs.readdirSync(context.directory); + const map = files.filter(file => file.includes(".map")); + + assert.ok(map.length > 0); + + const data = fs.readFileSync(path.resolve(context.directory, map[0])); + const mapObj = JSON.parse(data); + + const fixtureBasicIndex = + sourceMapProtocols + .map(protocol => + mapObj.sources.indexOf( + `${protocol}babel-loader/./test/fixtures/basic.js`, + ), + ) + .find(index => index !== -1) ?? -1; + // The index may vary across webpack versions + assert.notStrictEqual(fixtureBasicIndex, -1); + + // Ensure that the map contains the original code, not the compiled src. + assert.strictEqual( + mapObj.sourcesContent[fixtureBasicIndex].includes("__esModule"), + false, + ); + }); + + test("should output devtoolModuleFilename option", async () => { + const config = Object.assign({}, globalConfig, { + devtool: "source-map", + output: { + path: context.directory, + devtoolModuleFilenameTemplate: + "=!=!=!=[absolute-resource-path]=!=!=!=", + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + presets: ["@babel/env"], + }, + }, + ], + }, + }); + + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + const files = fs.readdirSync(context.directory); + const map = files.filter(file => file.includes(".map")); + + assert.ok(map.length > 0); + + const sourceMapContent = fs.readFileSync( + path.resolve(context.directory, map[0]), + "utf8", + ); + + assert.match( + sourceMapContent, + new RegExp( + JSON.stringify( + `=!=!=!=${globalConfig.entry.replace( + // Webpack 5, webpack 4, windows, linux, ... + /\\/g, + "(?:/|\\\\)", + )}=!=!=!=`, + ), + ), + ); + }); + + test("should disable sourcemap output with 'sourceMaps:false'", async () => { + const config = Object.assign({}, globalConfig, { + devtool: "source-map", + output: { + path: context.directory, }, - ], - }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + sourceMaps: false, + presets: [["@babel/env", { modules: "commonjs" }]], + }, + }, + ], + }, + }); + + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + const files = fs.readdirSync(context.directory); + const map = files.filter(file => file.includes(".map")); + + assert.ok(map.length > 0); + + const data = fs.readFileSync(path.resolve(context.directory, map[0])); + const mapObj = JSON.parse(data); + + const fixtureBasicIndex = + sourceMapProtocols + .map(protocol => + mapObj.sources.indexOf( + `${protocol}babel-loader/./test/fixtures/basic.js`, + ), + ) + .find(index => index !== -1) ?? -1; + // The index may vary across webpack versions + assert.notStrictEqual(fixtureBasicIndex, -1); + + // Ensure that the code contains Babel's compiled output, because + // sourcemaps from Babel are disabled. + assert.ok( + mapObj.sourcesContent[fixtureBasicIndex].includes("__esModule"), + ); + }); + + test("should disable sourcemap output with 'sourceMap:false'", async () => { + const config = Object.assign({}, globalConfig, { + devtool: "source-map", + output: { + path: context.directory, + }, + module: { + rules: [ + { + test: /\.jsx?/, + loader: babelLoader, + exclude: /node_modules/, + options: { + sourceMap: false, + presets: [["@babel/env", { modules: "commonjs" }]], + }, + }, + ], + }, + }); + + const stats = await bundler.compileAsync(config); + assert.equal(stats.compilation.errors.length, 0); + assert.equal(stats.compilation.warnings.length, 0); + + const files = fs.readdirSync(context.directory); + const map = files.filter(file => file.includes(".map")); + + assert.ok(map.length > 0); + + const data = fs.readFileSync(path.resolve(context.directory, map[0])); + const mapObj = JSON.parse(data); + + const fixtureBasicIndex = + sourceMapProtocols + .map(protocol => + mapObj.sources.indexOf( + `${protocol}babel-loader/./test/fixtures/basic.js`, + ), + ) + .find(index => index !== -1) ?? -1; + // The index may vary across webpack versions + assert.notStrictEqual(fixtureBasicIndex, -1); + + // Ensure that the code contains Babel's compiled output, because + // sourcemaps from Babel are disabled. + assert.ok( + mapObj.sourcesContent[fixtureBasicIndex].includes("__esModule"), + ); + }); }); - - const stats = await webpackAsync(config); - assert.deepEqual(stats.compilation.errors, []); - assert.deepEqual(stats.compilation.warnings, []); - - const files = fs.readdirSync(context.directory); - const map = files.filter(file => file.includes(".map")); - - assert.ok(map.length > 0); - - const data = fs.readFileSync(path.resolve(context.directory, map[0])); - const mapObj = JSON.parse(data); - - const fixtureBasicIndex = mapObj.sources.indexOf( - "webpack://babel-loader/./test/fixtures/basic.js", - ); - // The index may vary across webpack versions - assert.notStrictEqual(fixtureBasicIndex, -1); - - // Ensure that the code contains Babel's compiled output, because - // sourcemaps from Babel are disabled. - assert.ok(mapObj.sourcesContent[fixtureBasicIndex].includes("__esModule")); -}); +} diff --git a/yarn.lock b/yarn.lock index 58e14bd5..65e493ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1178,6 +1178,34 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:^1.5.0": + version: 1.8.1 + resolution: "@emnapi/core@npm:1.8.1" + dependencies: + "@emnapi/wasi-threads": 1.1.0 + tslib: ^2.4.0 + checksum: 2a2fb36f4e2f90e25f419f8979435160313664bbb833d852d9de4487ff47f05fd36bf2cd77c3555f704ec2b67ce3a949ed5542598664c775cdd5ef35ae1c85a4 + languageName: node + linkType: hard + +"@emnapi/runtime@npm:^1.5.0": + version: 1.8.1 + resolution: "@emnapi/runtime@npm:1.8.1" + dependencies: + tslib: ^2.4.0 + checksum: 0000a91d2d0ec3aaa37cbab9c360de3ff8250592f3ce4706b8c9c6d93e54151e623a8983c85543f33cb6f66cf30bb24bf0ddde466de484d6a6bf1fb2650382de + languageName: node + linkType: hard + +"@emnapi/wasi-threads@npm:1.1.0": + version: 1.1.0 + resolution: "@emnapi/wasi-threads@npm:1.1.0" + dependencies: + tslib: ^2.4.0 + checksum: 6cffe35f3e407ae26236092991786db5968b4265e6e55f4664bf6f2ce0508e2a02a44ce6ebb16f2acd2f6589efb293f4f9d09cc9fbf80c00fc1a203accc94196 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -1352,6 +1380,72 @@ __metadata: languageName: node linkType: hard +"@module-federation/error-codes@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/error-codes@npm:0.22.0" + checksum: 624d9ecae4dd97394eb679ad82c9befc7ce2a0dc160c10c4c8442a84ab206a184dc5985e1cc923faef70034f680f28547ae78b7e1783e78a79c54b22e132c64d + languageName: node + linkType: hard + +"@module-federation/runtime-core@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/runtime-core@npm:0.22.0" + dependencies: + "@module-federation/error-codes": 0.22.0 + "@module-federation/sdk": 0.22.0 + checksum: cf524475bbd576325c1d2fa26fc48d61f5434eb714d5e0c74ba961ff657f07aa50daf1f55ef78f6671187218022428e17b3fd5f8d8438d315b3283c7c3cccd30 + languageName: node + linkType: hard + +"@module-federation/runtime-tools@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/runtime-tools@npm:0.22.0" + dependencies: + "@module-federation/runtime": 0.22.0 + "@module-federation/webpack-bundler-runtime": 0.22.0 + checksum: 0e7693c1ec02fc5bef770b478c8757cad9cfefb2310d1943151d0ad079b72472d9b2c8a087299e9124dfcd6b649c83290c7fdfa333865baab4ba193f39e7b6bd + languageName: node + linkType: hard + +"@module-federation/runtime@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/runtime@npm:0.22.0" + dependencies: + "@module-federation/error-codes": 0.22.0 + "@module-federation/runtime-core": 0.22.0 + "@module-federation/sdk": 0.22.0 + checksum: eca608be999d7d2e83abc1169643c2f795a5ed950f9e2bdf7000400a30b3e1e0ca4bdaa5daa09f55e44868383d444707e40236cec1aaa7b40432b0cce800b7f3 + languageName: node + linkType: hard + +"@module-federation/sdk@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/sdk@npm:0.22.0" + checksum: 5f11f7032a9cb739265d6d9c93534ea70caeb1652d72afb8a6e8ea1b8cc3697943e295ad462fb8fcac2e12d5c3d7c882c2aec2b2428bb10d88b2627292543068 + languageName: node + linkType: hard + +"@module-federation/webpack-bundler-runtime@npm:0.22.0": + version: 0.22.0 + resolution: "@module-federation/webpack-bundler-runtime@npm:0.22.0" + dependencies: + "@module-federation/runtime": 0.22.0 + "@module-federation/sdk": 0.22.0 + checksum: a46cd5b7fba2481e4178aead6953aed9ba0be884ef2fc90ecce5356bbc8c299ff53e49c0feda10c8338eff38002c77e15d86c4747d3f7b7580d2a0de9f0510bd + languageName: node + linkType: hard + +"@napi-rs/wasm-runtime@npm:1.0.7": + version: 1.0.7 + resolution: "@napi-rs/wasm-runtime@npm:1.0.7" + dependencies: + "@emnapi/core": ^1.5.0 + "@emnapi/runtime": ^1.5.0 + "@tybys/wasm-util": ^0.10.1 + checksum: 9b59bd8b7310936ed163935befae0613dfffd563e7ff021d4f1b62b419fb0e3395f7206b17460a91db555bea6c471408f3472455e4e2ca9f5a0bff4468fa38d0 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1415,6 +1509,149 @@ __metadata: languageName: node linkType: hard +"@rspack/binding-darwin-arm64@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-darwin-arm64@npm:1.7.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rspack/binding-darwin-x64@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-darwin-x64@npm:1.7.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rspack/binding-linux-arm64-gnu@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rspack/binding-linux-arm64-musl@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rspack/binding-linux-x64-gnu@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rspack/binding-linux-x64-musl@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-x64-musl@npm:1.7.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rspack/binding-wasm32-wasi@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-wasm32-wasi@npm:1.7.5" + dependencies: + "@napi-rs/wasm-runtime": 1.0.7 + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rspack/binding-win32-arm64-msvc@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rspack/binding-win32-ia32-msvc@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rspack/binding-win32-x64-msvc@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rspack/binding@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding@npm:1.7.5" + dependencies: + "@rspack/binding-darwin-arm64": 1.7.5 + "@rspack/binding-darwin-x64": 1.7.5 + "@rspack/binding-linux-arm64-gnu": 1.7.5 + "@rspack/binding-linux-arm64-musl": 1.7.5 + "@rspack/binding-linux-x64-gnu": 1.7.5 + "@rspack/binding-linux-x64-musl": 1.7.5 + "@rspack/binding-wasm32-wasi": 1.7.5 + "@rspack/binding-win32-arm64-msvc": 1.7.5 + "@rspack/binding-win32-ia32-msvc": 1.7.5 + "@rspack/binding-win32-x64-msvc": 1.7.5 + dependenciesMeta: + "@rspack/binding-darwin-arm64": + optional: true + "@rspack/binding-darwin-x64": + optional: true + "@rspack/binding-linux-arm64-gnu": + optional: true + "@rspack/binding-linux-arm64-musl": + optional: true + "@rspack/binding-linux-x64-gnu": + optional: true + "@rspack/binding-linux-x64-musl": + optional: true + "@rspack/binding-wasm32-wasi": + optional: true + "@rspack/binding-win32-arm64-msvc": + optional: true + "@rspack/binding-win32-ia32-msvc": + optional: true + "@rspack/binding-win32-x64-msvc": + optional: true + checksum: b7d4ce199b5d3d88d5841382deefcb2e5ad46b214977472b3898729978e664d3ab15844f9084abda47e9aa03b00fb3763acf6fe9fd02b9ab779dbb4ae6623019 + languageName: node + linkType: hard + +"@rspack/core@npm:^1.7.5": + version: 1.7.5 + resolution: "@rspack/core@npm:1.7.5" + dependencies: + "@module-federation/runtime-tools": 0.22.0 + "@rspack/binding": 1.7.5 + "@rspack/lite-tapable": 1.1.0 + peerDependencies: + "@swc/helpers": ">=0.5.1" + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 35ebcb1d0b050327ab5fd3b92c7a1fd66929a6412ec2c08979ba7c6439fd8ab33e38a166ce06ff6f3e73ae5479e65c15b0fa95ddf97d996f72948010d1f56ee2 + languageName: node + linkType: hard + +"@rspack/lite-tapable@npm:1.1.0": + version: 1.1.0 + resolution: "@rspack/lite-tapable@npm:1.1.0" + checksum: 7b74b5577cca5fb5be52bee8ce5c4415383ab84bdbb1eaa910b5a20aa3a6bbecd822c4d140239320d311153a3de56f3388c109c04da09d52d6c103c8e9439588 + languageName: node + linkType: hard + +"@tybys/wasm-util@npm:^0.10.1": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" + dependencies: + tslib: ^2.4.0 + checksum: b8b281ffa9cd01cb6d45a4dddca2e28fd0cb6ad67cf091ba4a73ac87c0d6bd6ce188c332c489e87c20b0750b0b6fe3b99e30e1cd2227ec16da692f51c778944e + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -1803,6 +2040,7 @@ __metadata: "@babel/core": ^8.0.0-beta.1 "@babel/eslint-parser": ^8.0.0-beta.1 "@babel/preset-env": ^8.0.0-beta.1 + "@rspack/core": ^1.7.5 c8: ^10.1.2 eslint: ^9.6.0 eslint-config-prettier: ^9.1.0 @@ -1816,7 +2054,13 @@ __metadata: webpack: ^5.93.0 peerDependencies: "@babel/core": ^7.12.0 || ^8.0.0-beta.1 + "@rspack/core": ^1.0.0 || ^2.0.0-0 webpack: ">=5.61.0" + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true languageName: unknown linkType: soft @@ -4143,6 +4387,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.4.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a + languageName: node + linkType: hard + "tslib@npm:^2.6.2": version: 2.6.3 resolution: "tslib@npm:2.6.3"