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 @@ +icon-square-small 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"