diff --git a/package-lock.json b/package-lock.json index 7326a1e..b370b78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1632,6 +1632,12 @@ "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", "dev": true }, + "@types/flat": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.3.tgz", + "integrity": "sha512-uG/4x6EXYbq4VDsBJLNDHQAQmtRPg3x4tAXcBspxlnEknz8NiJxnHoxSiJKGNExiS00q4mJNvuEBgVA3jsDIdQ==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -4034,6 +4040,11 @@ "path-exists": "^4.0.0" } }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", diff --git a/package.json b/package.json index 9e51429..6f40974 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ ], "dependencies": { "clone-deep": "^4.0.1", + "flat": "^5.0.2", "wildcard": "^2.0.0" }, "devDependencies": { "@types/estree": "0.0.48", + "@types/flat": "^5.0.3", "husky": "^6.0.0", "prettier": "^2.3.1", "tsdx": "^0.14.1", diff --git a/src/index.ts b/src/index.ts index 81fe684..6ee32e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import { ICustomizeOptions, Key, } from "./types"; -import { isPlainObject, isUndefined } from "./utils"; +import { isPlainObject, isSameCondition, isUndefined } from "./utils"; function merge( firstConfiguration: Configuration | Configuration[], @@ -156,7 +156,7 @@ function mergeWithRule({ const bMatches = b.filter((o) => { const matches = rulesToMatch.every( - (rule) => ao[rule]?.toString() === o[rule]?.toString() + (rule) => isSameCondition(ao[rule], o[rule]) ); if (matches) { diff --git a/src/utils.ts b/src/utils.ts index 2fb757c..ea443b7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { flatten } from "flat"; + function isRegex(o) { return o instanceof RegExp; } @@ -21,4 +23,59 @@ function isUndefined(a) { return typeof a === "undefined"; } -export { isRegex, isFunction, isPlainObject, isUndefined }; +/** + * According to Webpack docs, a "test" should be the following: + * + * - A string + * - A RegExp + * - A function + * - An array of conditions (may be nested) + * - An object of conditions (may be nested) + * + * https://webpack.js.org/configuration/module/#condition + */ +function isSameCondition(a, b) { + if (!a || !b) { + return a === b; + } + if ( + typeof a === 'string' || typeof b === 'string' || + isRegex(a) || isRegex(b) || + isFunction(a) || isFunction(b) + ) { + return a.toString() === b.toString(); + } + + const entriesA = Object.entries(flatten(a)); + const entriesB = Object.entries(flatten(b)); + if (entriesA.length !== entriesB.length) { + return false; + } + + for (let i = 0; i < entriesA.length; i++) { + entriesA[i][0] = entriesA[i][0].replace(/\b\d+\b/g, "[]"); + entriesB[i][0] = entriesB[i][0].replace(/\b\d+\b/g, "[]"); + } + + function cmp([k1, v1], [k2, v2]) { + if (k1 < k2) return -1; + if (k1 > k2) return 1; + if (v1 < v2) return -1; + if (v1 > v2) return 1; + return 0; + }; + entriesA.sort(cmp); + entriesB.sort(cmp); + + if (entriesA.length !== entriesB.length) { + return false; + } + for (let i = 0; i < entriesA.length; i++) { + if (entriesA[i][0] !== entriesB[i][0] || entriesA[i][1]?.toString() !== entriesB[i][1]?.toString()) { + return false; + } + } + return true; +} + +export { isRegex, isFunction, isPlainObject, isUndefined, isSameCondition }; diff --git a/test/merge-with-rules.test.ts b/test/merge-with-rules.test.ts index 0a52219..f1a8217 100644 --- a/test/merge-with-rules.test.ts +++ b/test/merge-with-rules.test.ts @@ -1263,4 +1263,380 @@ describe("Merge with rules", function () { })(base, development) ).toEqual(result); }); + + it("should distinguish different object-formed test", () => { + const conf1 = { + module: { + rules: [ + { + test: { + and: [/\.less$/, /lib-a/], + }, + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { a: 1 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const conf2 = { + module: { + rules: [ + { + test: { + and: [/\.less$/, /lib-b/] + }, + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { b: 2 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const expected = { + module: { + rules: [ + { + test: { + and: [/\.less$/, /lib-a/], + }, + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { a: 1 }, + }, + }, + }, + ], + }, + { + test: { + and: [/\.less$/, /lib-b/], + }, + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { b: 2 }, + }, + }, + }, + ], + }, + ], + }, + }; + + expect( + mergeWithRules({ + module: { + rules: { + test: CustomizeRule.Match, + use: { + loader: CustomizeRule.Match, + options: CustomizeRule.Replace, + }, + }, + }, + })(conf1, conf2) + ).toEqual(expected); + }); + + it("should recognize the same object-formed test", () => { + const conf1 = { + module: { + rules: [ + { + test: { + and: [/\.less$/, /lib-a/], + }, + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { a: 1 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const conf2 = { + module: { + rules: [ + { + test: { + and: [/\.less$/, /lib-a/] + }, + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { b: 2 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const expected = { + module: { + rules: [ + { + test: { + and: [/\.less$/, /lib-a/], + }, + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { + b: 2, + }, + }, + }, + }, + ], + }, + ], + }, + }; + + expect( + mergeWithRules({ + module: { + rules: { + test: CustomizeRule.Match, + use: { + loader: CustomizeRule.Match, + options: CustomizeRule.Replace, + }, + }, + }, + })(conf1, conf2) + ).toEqual(expected); + }); + + it("should distinguish different array-formed test", () => { + const conf1 = { + module: { + rules: [ + { + test: [ + /\.less$/, + /lib-a/, + { not: [/exclude/] }, + ], + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { a: 1 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const conf2 = { + module: { + rules: [ + { + test: [ + /\.less$/, + /lib-b/, + { not: [/exclude/] }, + ], + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { b: 2 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const expected = { + module: { + rules: [ + { + test: [ + /\.less$/, + /lib-a/, + { not: [/exclude/] }, + ], + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { a: 1 }, + }, + }, + }, + ], + }, + { + test: [ + /\.less$/, + /lib-b/, + { not: [/exclude/] }, + ], + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { b: 2 }, + }, + }, + }, + ], + }, + ], + }, + }; + + expect( + mergeWithRules({ + module: { + rules: { + test: CustomizeRule.Match, + use: { + loader: CustomizeRule.Match, + options: CustomizeRule.Replace, + }, + }, + }, + })(conf1, conf2) + ).toEqual(expected); + }); + + it("should recognize the same array-formed test (even with different order)", () => { + const conf1 = { + module: { + rules: [ + { + test: [ + /\.less$/, + /lib-a/, + { not: [/exclude/] }, + ], + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { a: 1 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const conf2 = { + module: { + rules: [ + { + test: [ + { not: [/exclude/] }, + /lib-a/, + /\.less$/, + ], + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { b: 2 }, + }, + }, + }, + ], + }, + ], + }, + }; + + const expected = { + module: { + rules: [ + { + test: [ + /\.less$/, + /lib-a/, + { not: [/exclude/] }, + ], + use: [ + { + loader: 'less-loader', + options: { + lessOptions: { + modifyVars: { + b: 2, + }, + }, + }, + }, + ], + }, + ], + }, + }; + + expect( + mergeWithRules({ + module: { + rules: { + test: CustomizeRule.Match, + use: { + loader: CustomizeRule.Match, + options: CustomizeRule.Replace, + }, + }, + }, + })(conf1, conf2) + ).toEqual(expected); + }); });