diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 11a4dbe2a81..9dfebc27c2a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -27,7 +27,7 @@ jobs:
node-version: 17.x
cache: "yarn"
- run: yarn --frozen-lockfile
- - uses: actions/cache@v1
+ - uses: actions/cache@v3
with:
path: .eslintcache
key: lint-${{ env.GITHUB_SHA }}
@@ -62,7 +62,7 @@ jobs:
- run: yarn --frozen-lockfile
- run: yarn link --frozen-lockfile || true
- run: yarn link webpack --frozen-lockfile
- - uses: actions/cache@v1
+ - uses: actions/cache@v3
with:
path: .jest-cache
key: jest-unit-${{ env.GITHUB_SHA }}
@@ -101,7 +101,7 @@ jobs:
- run: yarn --frozen-lockfile
- run: yarn link --frozen-lockfile || true
- run: yarn link webpack --frozen-lockfile
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
with:
path: .jest-cache
key: jest-integration-${{ env.GITHUB_SHA }}
diff --git a/README.md b/README.md
index c712d27fd7a..a6549c1c462 100644
--- a/README.md
+++ b/README.md
@@ -158,11 +158,11 @@ or are automatically applied via regex from your webpack configuration.
#### Transpiling
-| Name | Status | Install Size | Description |
-| :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------: | :------------: | :------------------------------------------------------------------------------------------------ |
-|
| ![babel-npm] | ![babel-size] | Loads ES2015+ code and transpiles to ES5 using Babel |
-|
| ![type-npm] | ![type-size] | Loads TypeScript like JavaScript |
-|
| ![coffee-npm] | ![coffee-size] | Loads CoffeeScript like JavaScript |
+| Name | Status | Install Size | Description |
+| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------: | :------------: | :------------------------------------------------------------------------------------------------ |
+|
| ![babel-npm] | ![babel-size] | Loads ES2015+ code and transpiles to ES5 using Babel |
+|
| ![type-npm] | ![type-size] | Loads TypeScript like JavaScript |
+|
| ![coffee-npm] | ![coffee-size] | Loads CoffeeScript like JavaScript |
[babel-npm]: https://img.shields.io/npm/v/babel-loader.svg
[babel-size]: https://packagephobia.com/badge?p=babel-loader
@@ -175,7 +175,7 @@ or are automatically applied via regex from your webpack configuration.
| Name | Status | Install Size | Description |
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------: | :--------------: | :-------------------------------------------------------------------------------------- |
-|
| ![html-npm] | ![html-size] | Exports HTML as string, requires references to static resources |
+|
| ![html-npm] | ![html-size] | Exports HTML as string, requires references to static resources |
|
| ![pug-npm] | ![pug-size] | Loads Pug templates and returns a function |
|
| ![pug3-npm] | ![pug3-size] | Compiles Pug to a function or HTML string, useful for use with Vue, React, Angular |
|
| ![md-npm] | ![md-size] | Compiles Markdown to HTML |
diff --git a/declarations/LoaderContext.d.ts b/declarations/LoaderContext.d.ts
index 3e9341423a7..f93a0890d2d 100644
--- a/declarations/LoaderContext.d.ts
+++ b/declarations/LoaderContext.d.ts
@@ -212,6 +212,12 @@ export interface LoaderRunnerLoaderContext {
* Example: "/abc/resource.js?query#frag"
*/
resource: string;
+
+ /**
+ * Target of compilation.
+ * Example: "web"
+ */
+ target: string;
}
type AdditionalData = {
diff --git a/lib/NormalModule.js b/lib/NormalModule.js
index 4d1264f9b3c..b3fababd63f 100644
--- a/lib/NormalModule.js
+++ b/lib/NormalModule.js
@@ -330,6 +330,8 @@ class NormalModule extends Module {
this._isEvaluatingSideEffects = false;
/** @type {WeakSet | undefined} */
this._addedSideEffectsBailout = undefined;
+ /** @type {Map} */
+ this._codeGeneratorData = new Map();
}
/**
@@ -1188,11 +1190,9 @@ class NormalModule extends Module {
runtimeRequirements.add(RuntimeGlobals.thisAsExports);
}
- /** @type {Map} */
- let data;
+ /** @type {function(): Map} */
const getData = () => {
- if (data === undefined) data = new Map();
- return data;
+ return this._codeGeneratorData;
};
const sources = new Map();
@@ -1223,7 +1223,7 @@ class NormalModule extends Module {
const resultEntry = {
sources,
runtimeRequirements,
- data
+ data: this._codeGeneratorData
};
return resultEntry;
}
@@ -1371,6 +1371,7 @@ class NormalModule extends Module {
write(this.error);
write(this._lastSuccessfulBuildMeta);
write(this._forceBuild);
+ write(this._codeGeneratorData);
super.serialize(context);
}
@@ -1403,6 +1404,7 @@ class NormalModule extends Module {
this.error = read();
this._lastSuccessfulBuildMeta = read();
this._forceBuild = read();
+ this._codeGeneratorData = read();
super.deserialize(context);
}
}
diff --git a/lib/dependencies/ImportParserPlugin.js b/lib/dependencies/ImportParserPlugin.js
index 151ff89adcc..718b0482828 100644
--- a/lib/dependencies/ImportParserPlugin.js
+++ b/lib/dependencies/ImportParserPlugin.js
@@ -137,7 +137,7 @@ class ImportParserPlugin {
if (importOptions.webpackInclude !== undefined) {
if (
!importOptions.webpackInclude ||
- importOptions.webpackInclude.constructor.name !== "RegExp"
+ !(importOptions.webpackInclude instanceof RegExp)
) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
@@ -146,13 +146,13 @@ class ImportParserPlugin {
)
);
} else {
- include = new RegExp(importOptions.webpackInclude);
+ include = importOptions.webpackInclude;
}
}
if (importOptions.webpackExclude !== undefined) {
if (
!importOptions.webpackExclude ||
- importOptions.webpackExclude.constructor.name !== "RegExp"
+ !(importOptions.webpackExclude instanceof RegExp)
) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
@@ -161,7 +161,7 @@ class ImportParserPlugin {
)
);
} else {
- exclude = new RegExp(importOptions.webpackExclude);
+ exclude = importOptions.webpackExclude;
}
}
if (importOptions.webpackExports !== undefined) {
diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js
index c10c7b16eaf..58bcc4a64b3 100644
--- a/lib/javascript/JavascriptParser.js
+++ b/lib/javascript/JavascriptParser.js
@@ -3635,17 +3635,27 @@ class JavascriptParser extends Parser {
return EMPTY_COMMENT_OPTIONS;
}
let options = {};
+ /** @type {unknown[]} */
let errors = [];
for (const comment of comments) {
const { value } = comment;
if (value && webpackCommentRegExp.test(value)) {
// try compile only if webpack options comment is present
try {
- const val = vm.runInNewContext(`(function(){return {${value}};})()`);
- Object.assign(options, val);
+ for (let [key, val] of Object.entries(
+ vm.runInNewContext(`(function(){return {${value}};})()`)
+ )) {
+ if (typeof val === "object" && val !== null) {
+ if (val.constructor.name === "RegExp") val = new RegExp(val);
+ else val = JSON.parse(JSON.stringify(val));
+ }
+ options[key] = val;
+ }
} catch (e) {
- e.comment = comment;
- errors.push(e);
+ const newErr = new Error(String(e.message));
+ newErr.stack = String(e.stack);
+ Object.assign(newErr, { comment });
+ errors.push(newErr);
}
}
}
diff --git a/lib/optimize/RealContentHashPlugin.js b/lib/optimize/RealContentHashPlugin.js
index 39493200c96..ba058b753a2 100644
--- a/lib/optimize/RealContentHashPlugin.js
+++ b/lib/optimize/RealContentHashPlugin.js
@@ -178,10 +178,43 @@ class RealContentHashPlugin {
}
}
if (hashToAssets.size === 0) return;
- const hashRegExp = new RegExp(
- Array.from(hashToAssets.keys(), quoteMeta).join("|"),
- "g"
+ const hashRegExps = Array.from(hashToAssets.keys(), quoteMeta).map(
+ hash => new RegExp(hash, "g")
);
+
+ /**
+ * @param {string} str string to be matched against all hashRegExps
+ * @returns {string[] | null} matches found
+ */
+ const hashMatch = str => {
+ /** @type {string[]} */
+ const results = [];
+ for (const hashRegExp of hashRegExps) {
+ const matches = str.match(hashRegExp);
+ if (matches) {
+ matches.forEach(match => results.push(match));
+ }
+ }
+ if (results.length) {
+ return results;
+ } else {
+ return null;
+ }
+ };
+
+ /**
+ * @param {string} str string to be replaced with all hashRegExps
+ * @param {function(string): string} fn replacement function to use when a hash is found
+ * @returns {string} replaced content
+ */
+ const hashReplace = (str, fn) => {
+ let result = str;
+ for (const hashRegExp of hashRegExps) {
+ result = result.replace(hashRegExp, fn);
+ }
+ return result;
+ };
+
await Promise.all(
assetsWithInfo.map(async asset => {
const { name, source, content, hashes } = asset;
@@ -198,7 +231,7 @@ class RealContentHashPlugin {
await cacheAnalyse.providePromise(name, etag, () => {
const referencedHashes = new Set();
let ownHashes = new Set();
- const inContent = content.match(hashRegExp);
+ const inContent = hashMatch(content);
if (inContent) {
for (const hash of inContent) {
if (hashes.has(hash)) {
@@ -298,7 +331,7 @@ ${referencingAssets
identifier,
etag,
() => {
- const newContent = asset.content.replace(hashRegExp, hash =>
+ const newContent = hashReplace(asset.content, hash =>
hashToNewHash.get(hash)
);
return new RawSource(newContent);
@@ -323,15 +356,12 @@ ${referencingAssets
identifier,
etag,
() => {
- const newContent = asset.content.replace(
- hashRegExp,
- hash => {
- if (asset.ownHashes.has(hash)) {
- return "";
- }
- return hashToNewHash.get(hash);
+ const newContent = hashReplace(asset.content, hash => {
+ if (asset.ownHashes.has(hash)) {
+ return "";
}
- );
+ return hashToNewHash.get(hash);
+ });
return new RawSource(newContent);
}
);
@@ -342,7 +372,6 @@ ${referencingAssets
for (const oldHash of hashesInOrder) {
const assets = hashToAssets.get(oldHash);
assets.sort(comparator);
- const hash = createHash(this._hashFunction);
await Promise.all(
assets.map(asset =>
asset.ownHashes.has(oldHash)
@@ -363,6 +392,7 @@ ${referencingAssets
});
let newHash = hooks.updateHash.call(assetsContent, oldHash);
if (!newHash) {
+ const hash = createHash(this._hashFunction);
for (const content of assetsContent) {
hash.update(content);
}
@@ -374,7 +404,7 @@ ${referencingAssets
await Promise.all(
assetsWithInfo.map(async asset => {
await computeNewContent(asset);
- const newName = asset.name.replace(hashRegExp, hash =>
+ const newName = hashReplace(asset.name, hash =>
hashToNewHash.get(hash)
);
diff --git a/package.json b/package.json
index 112ccc561e0..2a604bfd149 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
"less": "^4.0.0",
"less-loader": "^8.0.0",
"lint-staged": "^11.0.0",
- "loader-utils": "^2.0.0",
+ "loader-utils": "^2.0.3",
"lodash": "^4.17.19",
"lodash-es": "^4.17.15",
"memfs": "^3.2.0",
diff --git a/test/Compiler-filesystem-caching.test.js b/test/Compiler-filesystem-caching.test.js
new file mode 100644
index 00000000000..cad5f679208
--- /dev/null
+++ b/test/Compiler-filesystem-caching.test.js
@@ -0,0 +1,152 @@
+"use strict";
+
+require("./helpers/warmup-webpack");
+
+const path = require("path");
+const fs = require("graceful-fs");
+const rimraf = require("rimraf");
+
+let fixtureCount = 0;
+
+describe("Compiler (filesystem caching)", () => {
+ jest.setTimeout(5000);
+
+ const tempFixturePath = path.join(
+ __dirname,
+ "fixtures",
+ "temp-filesystem-cache-fixture"
+ );
+
+ function compile(entry, onSuccess, onError) {
+ const webpack = require("..");
+ const options = webpack.config.getNormalizedWebpackOptions({});
+ options.cache = {
+ type: "filesystem",
+ cacheDirectory: path.join(tempFixturePath, "cache")
+ };
+ options.entry = entry;
+ options.context = path.join(__dirname, "fixtures");
+ options.output.path = path.join(tempFixturePath, "dist");
+ options.output.filename = "bundle.js";
+ options.output.pathinfo = true;
+ options.module = {
+ rules: [
+ {
+ test: /\.svg$/,
+ type: "asset/resource",
+ use: {
+ loader: require.resolve("./fixtures/empty-svg-loader")
+ }
+ }
+ ]
+ };
+
+ function runCompiler(onSuccess, onError) {
+ const c = webpack(options);
+ c.hooks.compilation.tap(
+ "CompilerCachingTest",
+ compilation => (compilation.bail = true)
+ );
+ c.run((err, stats) => {
+ if (err) throw err;
+ expect(typeof stats).toBe("object");
+ stats = stats.toJson({
+ modules: true,
+ reasons: true
+ });
+ expect(typeof stats).toBe("object");
+ expect(stats).toHaveProperty("errors");
+ expect(Array.isArray(stats.errors)).toBe(true);
+ if (stats.errors.length > 0) {
+ onError(new Error(JSON.stringify(stats.errors, null, 4)));
+ }
+ c.close(() => {
+ onSuccess(stats);
+ });
+ });
+ }
+
+ runCompiler(onSuccess, onError);
+
+ return {
+ runAgain: runCompiler
+ };
+ }
+
+ function cleanup() {
+ rimraf.sync(`${tempFixturePath}*`);
+ }
+
+ beforeAll(cleanup);
+ afterAll(cleanup);
+
+ function createTempFixture() {
+ const fixturePath = `${tempFixturePath}-${fixtureCount}`;
+ const usesAssetFilepath = path.join(fixturePath, "uses-asset.js");
+ const svgFilepath = path.join(fixturePath, "file.svg");
+
+ // Remove previous copy if present
+ rimraf.sync(fixturePath);
+
+ // Copy over file since we"ll be modifying some of them
+ fs.mkdirSync(fixturePath);
+ fs.copyFileSync(
+ path.join(__dirname, "fixtures", "uses-asset.js"),
+ usesAssetFilepath
+ );
+ fs.copyFileSync(path.join(__dirname, "fixtures", "file.svg"), svgFilepath);
+
+ fixtureCount++;
+ return {
+ rootPath: fixturePath,
+ usesAssetFilepath: usesAssetFilepath,
+ svgFilepath: svgFilepath
+ };
+ }
+
+ it("should compile again when cached asset has changed but loader output remains the same", done => {
+ const tempFixture = createTempFixture();
+
+ const onError = error => done(error);
+
+ const helper = compile(
+ tempFixture.usesAssetFilepath,
+ stats => {
+ // Not cached the first time
+ expect(stats.assets[0].name).toBe("bundle.js");
+ expect(stats.assets[0].emitted).toBe(true);
+
+ expect(stats.assets[1].name).toMatch(/\w+\.svg$/);
+ expect(stats.assets[0].emitted).toBe(true);
+
+ helper.runAgain(stats => {
+ // Cached the second run
+ expect(stats.assets[0].name).toBe("bundle.js");
+ expect(stats.assets[0].emitted).toBe(false);
+
+ expect(stats.assets[1].name).toMatch(/\w+\.svg$/);
+ expect(stats.assets[0].emitted).toBe(false);
+
+ const svgContent = fs
+ .readFileSync(tempFixture.svgFilepath)
+ .toString()
+ .replace("icon-square-small", "icon-square-smaller");
+
+ fs.writeFileSync(tempFixture.svgFilepath, svgContent);
+
+ helper.runAgain(stats => {
+ // Still cached after file modification because loader always returns empty
+ expect(stats.assets[0].name).toBe("bundle.js");
+ expect(stats.assets[0].emitted).toBe(false);
+
+ expect(stats.assets[1].name).toMatch(/\w+\.svg$/);
+ expect(stats.assets[0].emitted).toBe(false);
+
+ done();
+ }, onError);
+ }, onError);
+ },
+ onError
+ );
+ });
+});
diff --git a/test/fixtures/empty-svg-loader.js b/test/fixtures/empty-svg-loader.js
new file mode 100644
index 00000000000..0a599e7d5d6
--- /dev/null
+++ b/test/fixtures/empty-svg-loader.js
@@ -0,0 +1 @@
+module.exports = () => "";
diff --git a/test/fixtures/file.svg b/test/fixtures/file.svg
new file mode 100644
index 00000000000..d7b7e40b4f8
--- /dev/null
+++ b/test/fixtures/file.svg
@@ -0,0 +1 @@
+
diff --git a/test/fixtures/uses-asset.js b/test/fixtures/uses-asset.js
new file mode 100644
index 00000000000..b3532c8b7fc
--- /dev/null
+++ b/test/fixtures/uses-asset.js
@@ -0,0 +1 @@
+import SVG from './file.svg';
diff --git a/types.d.ts b/types.d.ts
index 251d0adfd3d..78da415cff2 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -6595,6 +6595,12 @@ declare interface LoaderRunnerLoaderContext {
* Example: "/abc/resource.js?query#frag"
*/
resource: string;
+
+ /**
+ * Target of compilation.
+ * Example: "web"
+ */
+ target: string;
}
declare class LoaderTargetPlugin {
constructor(target: string);
diff --git a/yarn.lock b/yarn.lock
index 951cf8d49d0..14ea915aa37 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4154,10 +4154,10 @@ loader-utils@^1.1.0, loader-utils@^1.4.0:
emojis-list "^3.0.0"
json5 "^1.0.1"
-loader-utils@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
- integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
+loader-utils@^2.0.0, loader-utils@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
+ integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"