diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index aa1460c..3b8b933 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,13 @@ N.B. See changelogs for individual packages, where most change will occur: This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). +## [0.13.0] - 2025-10-21 + +### Changed + +- updated to `0.11.0` of [`eslint-plugin-workspaces`](https://github.com/joshuajaco/eslint-plugin-workspaces) after [addition of ESLint9 support](https://github.com/joshuajaco/eslint-plugin-workspaces/commit/af855c3a3d8069366d4446747e91828ddf7560c6) + - update `eslint.config.mjs` to utilise flat config + ## [0.12.0] - 2025-09-30 ### Added diff --git a/eslint.config.mjs b/eslint.config.mjs index 93f947b..3ffc16a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,19 +2,13 @@ import asosConfig from "./peripheral/eslint-config-asosconfig/index.js"; import globals from "globals"; import jsdoc from "eslint-plugin-jsdoc"; import markdown from "@eslint/markdown"; -import { FlatCompat } from "@eslint/eslintrc"; -import path from "path"; -import { fileURLToPath } from "url"; +import workspaces from "eslint-plugin-workspaces"; const scripts = ["*.{js,mjs}", "**/*.{js,mjs}"]; const markDowns = ["*.md", "**/*.md"]; -const compat = new FlatCompat({ - baseDirectory: path.dirname(fileURLToPath(import.meta.url)) -}); - export default [ - ...compat.extends("plugin:workspaces/recommended"), + workspaces.configs["flat/recommended"], ...asosConfig.map((config) => ({ files: scripts, ignores: ["**/docs/**", "**/danger/**"], diff --git a/examples/express/docs/CHANGELOG.md b/examples/express/docs/CHANGELOG.md index 606436e..27f151f 100644 --- a/examples/express/docs/CHANGELOG.md +++ b/examples/express/docs/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.1] - 2025-10-21 + +### Fixed + +- removed "Vary" header from "animals" example, the page is meant to be un-cacheable, and the value was wrong in any case + ## [0.3.0] - 2025-10-20 ### Changed diff --git a/examples/express/package.json b/examples/express/package.json index 8ae5dc2..cbcb37c 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-express-example", - "version": "0.3.0", + "version": "0.3.1", "type": "module", "engines": { "node": ">=20.6.0" diff --git a/examples/express/src/routes/animals/middleware.js b/examples/express/src/routes/animals/middleware.js index b0be783..a772845 100644 --- a/examples/express/src/routes/animals/middleware.js +++ b/examples/express/src/routes/animals/middleware.js @@ -8,7 +8,6 @@ const contextMiddleware = (request, response, scopeCallBack) => { response.status(StatusCodes.BAD_REQUEST).end(); return; } - response.header("Vary", version); featuresStore.setValue({ value: { version }, scopeCallBack }); }; diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index 2c7f1d0..244ad2d 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.1] - 2025-10-21 + +### Changed + +- update to take supply static `webpackNormalModule` corresponding to webpack plugin [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) + ## [0.4.0] - 2025-10-20 ### Changed diff --git a/examples/next/package.json b/examples/next/package.json index 698a568..fb71445 100644 --- a/examples/next/package.json +++ b/examples/next/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-next-example", - "version": "0.4.0", + "version": "0.4.1", "private": true, "type": "module", "scripts": { diff --git a/examples/next/playwright.config.ts b/examples/next/playwright.config.ts index 331c13c..88d6b5e 100644 --- a/examples/next/playwright.config.ts +++ b/examples/next/playwright.config.ts @@ -1,5 +1,4 @@ import { defineConfig, type PlaywrightTestConfig } from "@playwright/test"; -// eslint-disable-next-line workspaces/no-relative-imports, workspaces/require-dependency import baseConfig from "../../test/automation/base.config"; const THREE_MINUTES = 3 * 60 * 1000; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/toggleHandlerFactory.ts b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/toggleHandlerFactory.ts index 9b2badc..138ada8 100644 --- a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/toggleHandlerFactory.ts +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/toggleHandlerFactory.ts @@ -10,13 +10,13 @@ interface ToggleHandler { featuresMap: Map> ) => React.Component; joinPoint: ReactComponentModuleType; - variants: __WebpackModuleApi.RequireContext; + variantPathMap: Map; } -export default ({ togglePoint, joinPoint, variants }: ToggleHandler) => { +export default ({ togglePoint, joinPoint, variantPathMap }: ToggleHandler) => { const variantsMap = new Map(); const featuresMap = new Map([[FEATURE_KEY, variantsMap]]); - for (const key of variants.keys()) { + for (const key of variantPathMap.keys()) { const [, , value] = key.split("."); const [start, end] = value.split("-"); @@ -25,7 +25,7 @@ export default ({ togglePoint, joinPoint, variants }: ToggleHandler) => { charCode <= end.charCodeAt(0); charCode++ ) { - variantsMap.set(String.fromCharCode(charCode), variants(key)); + variantsMap.set(String.fromCharCode(charCode), variantPathMap.get(key)!); } } diff --git a/examples/serve/docs/CHANGELOG.md b/examples/serve/docs/CHANGELOG.md index 6471442..ad96a31 100644 --- a/examples/serve/docs/CHANGELOG.md +++ b/examples/serve/docs/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - 2025-10-21 + +### Changed + +- updated toggle handlers to take a `variantPathMap` corresponding to webpack [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) + ## [0.3.0] - 2025-10-20 ### Changed diff --git a/examples/serve/package.json b/examples/serve/package.json index c6a5b97..e4bd084 100644 --- a/examples/serve/package.json +++ b/examples/serve/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-serve-example", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "private": true, "scripts": { diff --git a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js b/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js index 6fc9234..e8189b7 100644 --- a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js +++ b/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js @@ -1,9 +1,9 @@ -export default ({ togglePoint, joinPoint, variants }) => { - const featuresMap = variants.keys().reduce((map, key) => { +export default ({ togglePoint, joinPoint, variantPathMap }) => { + const featuresMap = variantPathMap.keys().reduce((map, key) => { const [, , value] = key.split("/"); const list = value.split(","); for (const value of list) { - map.set(value, variants(key)); + map.set(value, variantPathMap.get(key)); } return map; }, new Map()); diff --git a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js b/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js index d9dc3bc..2d9a6a3 100644 --- a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js +++ b/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js @@ -1,8 +1,8 @@ -export default ({ togglePoint, joinPoint, variants }) => { +export default ({ togglePoint, joinPoint, variantPathMap }) => { const featuresMap = new Map(); - for (const key of variants.keys()) { + for (const key of variantPathMap.keys()) { const [, , value] = key.split("."); - featuresMap.set(value, variants(key)); + featuresMap.set(value, variantPathMap.get(key)); } return togglePoint(joinPoint, featuresMap); }; diff --git a/examples/serve/src/toggleHandlers/singlePathSegment.js b/examples/serve/src/toggleHandlers/singlePathSegment.js index 9cf4f2d..7a72925 100644 --- a/examples/serve/src/toggleHandlers/singlePathSegment.js +++ b/examples/serve/src/toggleHandlers/singlePathSegment.js @@ -1,8 +1,8 @@ -export default ({ togglePoint, joinPoint, variants }) => { +export default ({ togglePoint, joinPoint, variantPathMap }) => { const featuresMap = new Map(); - for (const key of variants.keys()) { + for (const key of variantPathMap.keys()) { const [, value] = key.split("/"); - featuresMap.set(value, variants(key)); + featuresMap.set(value, variantPathMap.get(key)); } return togglePoint(joinPoint, featuresMap); }; diff --git a/package-lock.json b/package-lock.json index c700e0b..461345b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@asos/web-toggle-point", - "version": "0.12.0", + "version": "0.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@asos/web-toggle-point", - "version": "0.12.0", + "version": "0.13.0", "license": "MIT", "workspaces": [ "packages/features", @@ -42,7 +42,7 @@ "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-workspaces": "^0.10.1", + "eslint-plugin-workspaces": "^0.11.0", "globals": "^15.12.0", "jsdoc": "^4.0.4", "lint-staged": "^15.2.10", @@ -57,7 +57,7 @@ }, "examples/express": { "name": "web-toggle-point-express-example", - "version": "0.3.0", + "version": "0.3.1", "dependencies": { "@asos/web-toggle-point-features": "file:../../packages/features", "@asos/web-toggle-point-react-pointcuts": "file:../../packages/react-pointcuts", @@ -89,7 +89,7 @@ }, "examples/next": { "name": "web-toggle-point-next-example", - "version": "0.4.0", + "version": "0.4.1", "dependencies": { "@asos/web-toggle-point-features": "file:../../packages/features", "@asos/web-toggle-point-react-pointcuts": "file:../../packages/react-pointcuts", @@ -117,7 +117,7 @@ }, "examples/serve": { "name": "web-toggle-point-serve-example", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@asos/web-toggle-point-features": "file:../../packages/features", "@asos/web-toggle-point-webpack": "file:../../packages/webpack", @@ -6147,9 +6147,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", @@ -8615,11 +8615,13 @@ } }, "node_modules/eslint-plugin-workspaces": { - "version": "0.10.1", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-workspaces/-/eslint-plugin-workspaces-0.11.0.tgz", + "integrity": "sha512-1Ol5QoV+IDBt/YiGCAXWKccKI3AAUSQUmaz0cw0at/MjgEPHvCQAkrv5U2p0C3YInd4sOfBzmyumhWFl6n6INQ==", "dev": true, "license": "MIT", "dependencies": { - "find-workspaces": "^0.3.0" + "find-workspaces": "^0.3.1" } }, "node_modules/eslint-scope": { @@ -16680,30 +16682,6 @@ "@babel/runtime": "^7.8.4" } }, - "node_modules/regexgen": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "jsesc": "^2.3.0", - "regenerate": "^1.3.2" - }, - "bin": { - "regexgen": "bin/cli.js" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/regexgen/node_modules/jsesc": { - "version": "2.5.2", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "license": "MIT", @@ -20209,12 +20187,11 @@ }, "packages/webpack": { "name": "@asos/web-toggle-point-webpack", - "version": "0.8.3", + "version": "0.9.0", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "fast-glob": "^3.2.12", - "regexgen": "^1.3.0" + "fast-glob": "^3.2.12" }, "devDependencies": { "@rollup/plugin-babel": "^6.0.2", @@ -20238,13 +20215,7 @@ "webpack-test-utils": "^2.1.0" }, "peerDependencies": { - "next": ">14", "webpack": ">=5.70" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } } }, "peripheral/babel-preset-asos": { diff --git a/package.json b/package.json index d142a93..7ceab75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@asos/web-toggle-point", - "version": "0.12.0", + "version": "0.13.0", "repository": "git@github.com:asos/web-toggle-point.git", "homepage": "https://asos.github.io/web-toggle-point/", "license": "MIT", @@ -47,7 +47,7 @@ "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-workspaces": "^0.10.1", + "eslint-plugin-workspaces": "^0.11.0", "globals": "^15.12.0", "jsdoc": "^4.0.4", "lint-staged": "^15.2.10", diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index 7be0917..f33559a 100644 --- a/packages/webpack/docs/CHANGELOG.md +++ b/packages/webpack/docs/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.0] - 2025-10-21 + +### Changed + +- consolidated setting of default optional values +- changed `variants` array on join point data structure to a `Map` of relative to absolute path as `variantPathMap` +- move away from webpack `import.meta.webpackContext` when generating join points, construct a `Map` manually instead + - add linking of join points, to supplant the functionality previously provided by `import.meta.webpackContext` +- updated win32 path replacement, can effectively no-op on posix systems + +### Fixed + +- removed "next" peer dependency, this needn't be explicit +- ensured files that cannot be resolved (by [enhanced-resolve](https://github.com/webpack/enhanced-resolve/)), for whatever reason, don't break the build +- don't try and filter potential resolutions, let enhanced-resolve try and potentially fail, to allow for resolve plugins to have irregular specifiers (e.g. path alias') +- ensured that circular dependencies don't cause the module graph search lock up +- correct README.md to show `toggleHandler` as an option of the `pointCut`, not the general plugin configuration + ## [0.8.3] - 2025-09-30 ### Fixed diff --git a/packages/webpack/docs/README.md b/packages/webpack/docs/README.md index da89522..1a32ee0 100644 --- a/packages/webpack/docs/README.md +++ b/packages/webpack/docs/README.md @@ -39,11 +39,11 @@ interface PointCut { togglePointModule: string; variantGlobs?: string[]; joinPointResolver?: (variantPath: string) => string; + toggleHandler?: string; } interface TogglePointInjectionOptions { pointCuts: PointCut[]; - toggleHandler?: string; webpackNormalModule?: typeof webpack.NormalModule; } ``` diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 548ff8d..cfaa680 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -1,7 +1,7 @@ { "name": "@asos/web-toggle-point-webpack", "description": "toggle point webpack plugin", - "version": "0.8.3", + "version": "0.9.0", "license": "MIT", "type": "module", "main": "./lib/main.cjs", @@ -43,8 +43,7 @@ }, "dependencies": { "@babel/runtime": "^7.26.0", - "fast-glob": "^3.2.12", - "regexgen": "^1.3.0" + "fast-glob": "^3.2.12" }, "devDependencies": { "@rollup/plugin-babel": "^6.0.2", @@ -68,12 +67,6 @@ "transform-markdown-links": "^2.1.0" }, "peerDependencies": { - "webpack": ">=5.70", - "next": ">14" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } + "webpack": ">=5.70" } } diff --git a/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js new file mode 100644 index 0000000..a23fbbe --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js @@ -0,0 +1,27 @@ +import { posix, basename } from "path"; +import webpack from "webpack"; + +const fillDefaultPointcutValues = (pointCut) => { + const { + variantGlobs = ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], + joinPointResolver = (variantPath) => + posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)), + toggleHandler = "@asos/web-toggle-point-webpack/pathSegmentToggleHandler" + } = pointCut; + return { + ...pointCut, + variantGlobs, + joinPointResolver, + toggleHandler + }; +}; + +const fillDefaultOptionalValues = (options) => { + return { + webpackNormalModule: webpack.NormalModule, + ...options, + pointCuts: options.pointCuts.map(fillDefaultPointcutValues) + }; +}; + +export default fillDefaultOptionalValues; diff --git a/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js new file mode 100644 index 0000000..7e5a2f4 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js @@ -0,0 +1,84 @@ +import webpack from "webpack"; +import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; + +jest.mock("webpack", () => ({ NormalModule: Symbol("test-normal-module") })); + +describe("fillDefaultOptionalValues", () => { + let result; + + const makeDefaultJoinPointResolverAssertions = () => { + describe("when the joinPointResolver is called", () => { + it("should return a path that is the same as the variantPath, but with the last 3 directories removed", () => { + const variantPath = + "/test-folder/test-sub-folder/test-sub-folder/test-sub-folder/test-variant"; + const joinPointPath = result.joinPointResolver(variantPath); + expect(joinPointPath).toEqual("/test-folder/test-variant"); + }); + }); + }; + + const variantGlobs = Symbol("test-variant-globs"); + const joinPointResolver = Symbol("test-join-point-resolver"); + + const defaultVariantGlobs = [ + "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}" + ]; + const defaultJoinPointResolver = expect.any(Function); + const defaultToggleHandler = + "@asos/web-toggle-point-webpack/pathSegmentToggleHandler"; + const toggleHandler = Symbol("test-toggle-handler"); + const webpackNormalModule = Symbol("test-webpack-normal-module"); + + describe("when configuring the plugin with a supplied webpackNormalModule", () => { + beforeEach(() => { + result = fillDefaultOptionalValues({ + webpackNormalModule, + pointCuts: [] + }); + }); + + it("should return the supplied webpackNormalModule", () => { + expect(result.webpackNormalModule).toBe(webpackNormalModule); + }); + }); + + describe("when configuring the plugin without supplying a webpackNormalModule", () => { + beforeEach(() => { + result = fillDefaultOptionalValues({ + pointCuts: [] + }); + }); + + it("should return the NormalModule from the webpack import", () => { + expect(result.webpackNormalModule).toBe(webpack.NormalModule); + }); + }); + + describe.each` + variantGlobs | joinPointResolver | toggleHandler | description | expectation + ${undefined} | ${undefined} | ${undefined} | ${"nothing"} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler: defaultToggleHandler }} + ${variantGlobs} | ${undefined} | ${undefined} | ${"a variantGlob, but nothing else"} | ${{ variantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler: defaultToggleHandler }} + ${variantGlobs} | ${joinPointResolver} | ${undefined} | ${"a variantGlob and a join point resolver"} | ${{ variantGlobs, joinPointResolver, toggleHandler: defaultToggleHandler }} + ${undefined} | ${joinPointResolver} | ${undefined} | ${"a joinPointResolver, but nothing else"} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver, toggleHandler: defaultToggleHandler }} + ${undefined} | ${undefined} | ${toggleHandler} | ${"a toggle handler "} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler }} + ${variantGlobs} | ${undefined} | ${toggleHandler} | ${"a toggle handler and a variantGlob, but nothing else"} | ${{ variantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler }} + ${variantGlobs} | ${joinPointResolver} | ${toggleHandler} | ${"a toggle handler, a variantGlob and a join point resolver"} | ${{ variantGlobs, joinPointResolver, toggleHandler }} + ${undefined} | ${joinPointResolver} | ${toggleHandler} | ${"a toggle handler and a joinPointResolver, but nothing else"} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver, toggleHandler }} + `( + "when configuring pointCuts, supplying $description", + // eslint-disable-next-line no-unused-vars + ({ expectation, description, ...pointCut }) => { + beforeEach(async () => { + result = fillDefaultOptionalValues({ pointCuts: [pointCut] }); + }); + + it("should fill the defaults", () => { + expect(result.pointCuts[0]).toEqual(expectation); + }); + + if (!joinPointResolver) { + makeDefaultJoinPointResolverAssertions(); + } + } + ); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/index.js b/packages/webpack/src/plugins/togglePointInjection/index.js index c48793f..9f10076 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.js @@ -1,12 +1,12 @@ import processPointCuts from "./processPointCuts/index.js"; import Logger from "./logger.js"; -import { win32, posix } from "path"; +import { sep, posix } from "path"; import { PLUGIN_NAME } from "./constants.js"; import resolveJoinPoints from "./resolveJoinPoints/index.js"; import setupSchemeModules from "./setupSchemeModules/index.js"; import { validate } from "schema-utils"; import schema from "./schema.json"; -import webpack from "webpack"; +import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; /** * Toggle Point Injection Plugin @@ -22,7 +22,7 @@ class TogglePointInjection { * @param {string} options.pointCuts[].togglePointModule path, from root of the compilation, of where the toggle point sits. Or a resolvable node_module. * @param {string[]} [options.pointCuts[].variantGlobs=[.\/**\/__variants__/*\/*\/!(*.test).{js,jsx,ts,tsx}]] {@link https://en.wikipedia.org/wiki/Glob_(programming)|Globs} to identified variant modules. The plugin uses {@link https://github.com/mrmlnc/fast-glob|fast-glob} under the hood, so supports any glob that it does. * @param {function} [options.pointCuts[].joinPointResolver=(variantPath) => path.posix.resolve(variantPath, "../../../..", path.basename(variantPath))] A function that takes the path to a variant module and returns a join point / base module. N.B. This is executed at build-time, so cannot use run-time context. It should use posix path segments, so on Windows be sure to use path.posix.resolve. - * @param {string} [options.pointCuts[].toggleHandler] Path to a toggle handler that unpicks a {@link https://webpack.js.org/api/module-methods/#requirecontext|require.context} containing potential variants, passing that plus a joint point module to a toggle point function. If not provided, the plugin will use a default handler that processes folder names into a tree held in a Map. Leaf nodes of the tree are the variant modules. + * @param {string} [options.pointCuts[].toggleHandler='@asos/web-toggle-point-webpack/pathSegmentToggleHandler'] a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_features_into_your_script|module specifier} pointing to a toggle handler, that takes a toggle point, a Map of relative paths to potential variants, and a join point. If not provided, the plugin will use a default handler that processes folder names into a tree held in a Map. * @param {function} [options.webpackNormalModule] A reference to the Webpack NormalModule class. This is required for Next.js, as it does not expose the NormalModule class directly * @returns {external:Webpack.WebpackPluginInstance} WebpackPluginInstance * @example N.B. forward slashes are escaped in the glob, due to JSDoc shortcomings, but in reality should be un-escaped @@ -45,16 +45,13 @@ class TogglePointInjection { */ constructor(options) { validate(schema, options, { name: PLUGIN_NAME, baseDataPath: "options" }); - this.options = { - webpackNormalModule: webpack.NormalModule, - ...options - }; + this.options = fillDefaultOptionalValues(options); } apply(compiler) { let NormalModule, joinPointFiles, warnings, appRoot; compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, async () => { - appRoot = compiler.context.replaceAll(win32.sep, posix.sep); + appRoot = compiler.context.replaceAll(sep, posix.sep); ({ joinPointFiles, warnings } = await processPointCuts({ appRoot, fileSystem: compiler.inputFileSystem, diff --git a/packages/webpack/src/plugins/togglePointInjection/index.test.js b/packages/webpack/src/plugins/togglePointInjection/index.test.js index a3ad580..d2db6bb 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.test.js @@ -4,7 +4,7 @@ import { PLUGIN_NAME } from "./constants.js"; import resolveJoinPoints from "./resolveJoinPoints/index.js"; import setupSchemeModules from "./setupSchemeModules/index.js"; import TogglePointInjection from "./index.js"; -import webpack from "webpack"; +import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; import { validate } from "schema-utils"; import schema from "./schema.json"; @@ -20,9 +20,15 @@ jest.mock("./constants", () => ({ jest.mock("./processPointCuts/index.js", () => jest.fn()); jest.mock("./resolveJoinPoints/index.js", () => jest.fn()); jest.mock("./setupSchemeModules/index.js", () => jest.fn()); -jest.mock("webpack", () => ({ NormalModule: Symbol("test-normal-module") })); jest.mock("schema-utils", () => ({ validate: jest.fn() })); jest.mock("./schema.json", () => Symbol("test-json")); +const mockNormalModule = Symbol("test-normal-module"); +jest.mock("./fillDefaultOptionalValues.js", () => + jest.fn((options) => ({ + ...options, + webpackNormalModule: mockNormalModule + })) +); describe("togglePointInjection", () => { let togglePointInjection, compiler, options; @@ -40,162 +46,145 @@ describe("togglePointInjection", () => { }; }); - const makeCommonAssertions = (NormalModule) => { - it("should validate the supplied options", () => { - expect(validate).toHaveBeenCalledWith( - schema, - options, - expect.objectContaining({ name: PLUGIN_NAME, baseDataPath: "options" }) - ); - }); - - it("should tap into the beforeCompile event, indicating the plugin name", () => { - expect(compiler.hooks.beforeCompile.tapPromise).toHaveBeenCalledWith( - PLUGIN_NAME, - expect.any(Function) - ); - }); - - it("should tap into the compilation event, indicating the plugin name", () => { - expect(compiler.hooks.compilation.tap).toHaveBeenCalledWith( - PLUGIN_NAME, - expect.any(Function) - ); + describe("when a webpackNormalModule option is not supplied", () => { + beforeEach(() => { + options = { pointCuts }; + togglePointInjection = new TogglePointInjection(options); }); - describe("when the beforeCompile event is triggered", () => { - let beforeCompileCallback, resolve, result; - const warnings = Symbol("test-warnings"); - const compilation = Symbol("test-compilation"); - const normalModuleFactory = Symbol("test-normal-module-factory"); - + describe("when applying to a compiler", () => { beforeEach(() => { - processPointCuts.mockReturnValue( - new Promise((res) => { - resolve = res; + togglePointInjection.apply(compiler); + }); + + it("should validate the supplied options", () => { + expect(validate).toHaveBeenCalledWith( + schema, + options, + expect.objectContaining({ + name: PLUGIN_NAME, + baseDataPath: "options" }) ); - [, beforeCompileCallback] = - compiler.hooks.beforeCompile.tapPromise.mock.lastCall; - result = beforeCompileCallback(); }); - it("should process the supplied point cuts, returning only when processed", () => { - expect(processPointCuts).toHaveBeenCalledWith({ - appRoot: compiler.context, - fileSystem: compiler.inputFileSystem, - options: togglePointInjection.options - }); + it("should fill in default optional values", () => { + expect(fillDefaultOptionalValues).toHaveBeenCalledWith(options); }); - const makeCommonAssertions = () => { - it("should create a logger for the compilation", () => { - expect(Logger).toHaveBeenCalledWith(compilation); - }); + it("should tap into the beforeCompile event, indicating the plugin name", () => { + expect(compiler.hooks.beforeCompile.tapPromise).toHaveBeenCalledWith( + PLUGIN_NAME, + expect.any(Function) + ); + }); + + it("should tap into the compilation event, indicating the plugin name", () => { + expect(compiler.hooks.compilation.tap).toHaveBeenCalledWith( + PLUGIN_NAME, + expect.any(Function) + ); + }); - it("should log any warnings", () => { - expect(Logger.mock.instances[0].logWarnings).toHaveBeenCalledWith( - warnings + describe("when the beforeCompile event is triggered", () => { + let beforeCompileCallback, resolve, result; + const warnings = Symbol("test-warnings"); + const compilation = Symbol("test-compilation"); + const normalModuleFactory = Symbol("test-normal-module-factory"); + + beforeEach(() => { + processPointCuts.mockReturnValue( + new Promise((res) => { + resolve = res; + }) ); + [, beforeCompileCallback] = + compiler.hooks.beforeCompile.tapPromise.mock.lastCall; + result = beforeCompileCallback(); }); - }; - - describe("when the point cuts are processed, and some are found", () => { - const joinPointFiles = new Set([Symbol("test-join-point-file")]); - beforeEach(async () => { - resolve({ joinPointFiles, warnings }); - await result; + it("should process the supplied point cuts, returning only when processed", () => { + expect(processPointCuts).toHaveBeenCalledWith({ + appRoot: compiler.context, + fileSystem: compiler.inputFileSystem, + options: togglePointInjection.options + }); }); - describe("when the compilation event is triggered", () => { - beforeEach(() => { - const [, compilationCallback] = - compiler.hooks.compilation.tap.mock.lastCall; - compilationCallback(compilation, { normalModuleFactory }); + const makeCommonAssertions = () => { + it("should create a logger for the compilation", () => { + expect(Logger).toHaveBeenCalledWith(compilation); }); - makeCommonAssertions(); - - it("should log the join points", () => { - expect(Logger.mock.instances[0].logJoinPoints).toHaveBeenCalledWith( - joinPointFiles + it("should log any warnings", () => { + expect(Logger.mock.instances[0].logWarnings).toHaveBeenCalledWith( + warnings ); }); + }; - it("should set up scheme modules", () => { - expect(setupSchemeModules).toHaveBeenCalledWith({ - NormalModule, - compilation, - joinPointFiles, - pointCuts - }); - }); + describe("when the point cuts are processed, and some are found", () => { + const joinPointFiles = new Set([Symbol("test-join-point-file")]); - it("should resolve the join points", () => { - expect(resolveJoinPoints).toHaveBeenCalledWith({ - compilation, - appRoot: compiler.context, - normalModuleFactory, - joinPointFiles - }); + beforeEach(async () => { + resolve({ joinPointFiles, warnings }); + await result; }); - }); - }); - - describe("when the point cuts are processed, and none are found", () => { - beforeEach(async () => { - resolve({ joinPointFiles: [], warnings }); - await result; - }); - describe("when the compilation event is triggered", () => { - beforeEach(() => { - const [, compilationCallback] = - compiler.hooks.compilation.tap.mock.lastCall; - compilationCallback(compilation, { - normalModuleFactory + describe("when the compilation event is triggered", () => { + beforeEach(() => { + const [, compilationCallback] = + compiler.hooks.compilation.tap.mock.lastCall; + compilationCallback(compilation, { normalModuleFactory }); }); - }); - makeCommonAssertions(); - }); - }); - }); - }; + makeCommonAssertions(); - describe("when a webpackNormalModule option is not supplied", () => { - beforeEach(() => { - options = { pointCuts }; - togglePointInjection = new TogglePointInjection(options); - }); + it("should log the join points", () => { + expect( + Logger.mock.instances[0].logJoinPoints + ).toHaveBeenCalledWith(joinPointFiles); + }); - describe("when applying to a compiler", () => { - beforeEach(() => { - togglePointInjection.apply(compiler); - }); + it("should set up scheme modules", () => { + expect(setupSchemeModules).toHaveBeenCalledWith({ + NormalModule: mockNormalModule, + compilation, + joinPointFiles, + pointCuts + }); + }); - makeCommonAssertions(webpack.NormalModule); - }); - }); + it("should resolve the join points", () => { + expect(resolveJoinPoints).toHaveBeenCalledWith({ + compilation, + appRoot: compiler.context, + normalModuleFactory, + joinPointFiles + }); + }); + }); + }); - describe("when a webpackNormalModule option is supplied (primarily for NextJs users to get around the fact that webpack is pre-bundled)", () => { - const MockNormalModule = Symbol("test-normal-module"); + describe("when the point cuts are processed, and none are found", () => { + beforeEach(async () => { + resolve({ joinPointFiles: [], warnings }); + await result; + }); - beforeEach(() => { - options = { - pointCuts, - webpackNormalModule: MockNormalModule - }; - togglePointInjection = new TogglePointInjection(options); - }); + describe("when the compilation event is triggered", () => { + beforeEach(() => { + const [, compilationCallback] = + compiler.hooks.compilation.tap.mock.lastCall; + compilationCallback(compilation, { + normalModuleFactory + }); + }); - describe("when applying to a compiler", () => { - beforeEach(() => { - togglePointInjection.apply(compiler); + makeCommonAssertions(); + }); + }); }); - - makeCommonAssertions(MockNormalModule); }); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/integration.test.js b/packages/webpack/src/plugins/togglePointInjection/integration.test.js index d28aa06..07ec825 100644 --- a/packages/webpack/src/plugins/togglePointInjection/integration.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/integration.test.js @@ -1,6 +1,6 @@ import { build } from "webpack-test-utils"; import { readFile } from "fs/promises"; -import { resolve } from "path"; +import { posix } from "path"; import TogglePointInjection from "./index.js"; import { PLUGIN_NAME } from "./constants.js"; @@ -34,7 +34,7 @@ describe("togglePointInjection", () => { fileSystem = { "node_modules/@asos/web-toggle-point-webpack/pathSegmentToggleHandler": await readFile( - resolve( + posix.resolve( __dirname, "..", "..", @@ -102,7 +102,7 @@ describe("togglePointInjection", () => { it("should log the fact that the toggle point was found", () => { expect(getLogOfType("info")).toEqual( - `Identified '${name}' point cut for join point '${modulesFolder}${moduleName}' with potential variants:\n./${variantsFolder}/${testFeature}/${testVariant}/${moduleName}` + `Identified '${name}' point cut for join point '${modulesFolder}${moduleName}' with potential variants:\n${modulesFolder}${variantsFolder}/${testFeature}/${testVariant}/${moduleName}` ); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/logger.js b/packages/webpack/src/plugins/togglePointInjection/logger.js index 07ad8da..d85b6b5 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.js @@ -11,14 +11,14 @@ class Logger { for (const [ joinPoint, { - variants, + variantPathMap, pointCut: { name } } ] of joinPointFiles.entries()) { this.#logger.info( - `Identified '${name}' point cut for join point '${joinPoint}' with potential variants:\n${variants.join( - "\n" - )}` + `Identified '${name}' point cut for join point '${joinPoint}' with potential variants:\n${[ + ...variantPathMap.values() + ].join("\n")}` ); } } diff --git a/packages/webpack/src/plugins/togglePointInjection/logger.test.js b/packages/webpack/src/plugins/togglePointInjection/logger.test.js index 915f876..6306bfe 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.test.js @@ -21,12 +21,15 @@ describe("logger", () => { describe("logJoinPoints", () => { const pointCut = { name: "test-point-cut" }; const joinPointName = "test-join-point"; - const variants = ["test-variant-1", "test-variant-2"]; + const variantPathMap = new Map([ + ["test-key-1", "test-path-1"], + ["test-key-2", "test-key-2"] + ]); const joinPointFiles = new Map([ [ joinPointName, { - variants, + variantPathMap, pointCut: { name: "test-point-cut" } } ] @@ -40,9 +43,9 @@ describe("logger", () => { expect(compilationLogger.info).toHaveBeenCalledWith( `Identified '${ pointCut.name - }' point cut for join point '${joinPointName}' with potential variants:\n${variants.join( - "\n" - )}` + }' point cut for join point '${joinPointName}' with potential variants:\n${Array.from( + variantPathMap.values() + ).join("\n")}` ); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js deleted file mode 100644 index fecdcd5..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js +++ /dev/null @@ -1,13 +0,0 @@ -import { posix, basename } from "path"; - -const fillPointCutDefaults = (pointCut) => { - const { - variantGlobs = ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], - joinPointResolver = (variantPath) => - posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)) - } = pointCut; - - return { variantGlobs, joinPointResolver }; -}; - -export default fillPointCutDefaults; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js deleted file mode 100644 index a199b19..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import fillPointCutDefaults from "./fillDefaultOptionalValues"; - -describe("fillDefaultOptionalValues", () => { - let result; - - const makeDefaultJoinPointResolverAssertions = () => { - describe("when the joinPointResolver is called", () => { - it("should return a path that is the same as the variantPath, but with the last 3 directories removed", () => { - const variantPath = - "/test-folder/test-sub-folder/test-sub-folder/test-sub-folder/test-variant"; - const joinPointPath = result.joinPointResolver(variantPath); - expect(joinPointPath).toEqual("/test-folder/test-variant"); - }); - }); - }; - - describe("when the point cut has no variantGlobs or joinPointResolver", () => { - const pointCut = {}; - - beforeEach(() => { - result = fillPointCutDefaults(pointCut); - }); - - it("should fill the defaults", () => { - expect(result).toEqual({ - variantGlobs: ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], - joinPointResolver: expect.any(Function) - }); - }); - - makeDefaultJoinPointResolverAssertions(); - }); - - describe("when the point cut has a variantGlobs but no joinPointResolver", () => { - const pointCut = { variantGlobs: Symbol("test-variant-globs") }; - - beforeEach(() => { - result = fillPointCutDefaults(pointCut); - }); - - it("should fill the defaults", () => { - expect(result).toEqual({ - variantGlobs: pointCut.variantGlobs, - joinPointResolver: expect.any(Function) - }); - }); - - makeDefaultJoinPointResolverAssertions(); - }); - - describe("when the point cut has a joinPointResolver but no variantGlobs", () => { - it("should return the supplied joinPointResolver and fill default variantGlobs", () => { - const pointCut = { - joinPointResolver: Symbol("test-join-point-resolver") - }; - const result = fillPointCutDefaults(pointCut); - expect(result).toEqual({ - variantGlobs: ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], - joinPointResolver: pointCut.joinPointResolver - }); - }); - }); - - describe("when the point cut has variantGlobs and a joinPointResolver", () => { - it("should return the point cut supplied values", () => { - const pointCut = { - variantGlobs: Symbol("test-variant-glob"), - joinPointResolver: Symbol("test-join-point-resolver") - }; - const result = fillPointCutDefaults(pointCut); - expect(result).toEqual(pointCut); - }); - }); -}); diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js index 503b057..ec0d590 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js @@ -1,6 +1,5 @@ import processVariantFiles from "./processVariantFiles/index.js"; import getVariantPaths from "./getVariantPaths.js"; -import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; const processPointCuts = async ({ appRoot, @@ -11,8 +10,7 @@ const processPointCuts = async ({ const configFiles = new Map(); const warnings = []; for await (const pointCut of pointCuts.values()) { - const { variantGlobs, joinPointResolver } = - fillDefaultOptionalValues(pointCut); + const { variantGlobs } = pointCut; const variantPaths = await getVariantPaths({ variantGlobs, @@ -24,7 +22,6 @@ const processPointCuts = async ({ variantPaths, joinPointFiles, pointCut, - joinPointResolver, warnings, configFiles, fileSystem, diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js index 62768a2..71f9ecd 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js @@ -1,18 +1,11 @@ import processVariantFiles from "./processVariantFiles/index.js"; import getVariantPaths from "./getVariantPaths.js"; import processPointCuts from "./index.js"; -import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; jest.mock("./processVariantFiles/index", () => jest.fn()); jest.mock("./getVariantPaths", () => jest.fn(() => Symbol("test-variant-files")) ); -jest.mock("./fillDefaultOptionalValues", () => - jest.fn(() => ({ - variantGlobs: Symbol("test-variant-globs"), - joinPointResolver: Symbol("test-join-point-resolver") - })) -); describe("processPointCuts", () => { const pointCuts = new Map([ @@ -34,16 +27,8 @@ describe("processPointCuts", () => { })); }); - it("should fill in default optional values for each point cut", () => { - for (const pointCut of pointCutsValues) { - expect(fillDefaultOptionalValues).toHaveBeenCalledWith(pointCut); - } - }); - it("should get variant files for each of the point cuts", () => { - for (const index of pointCutsValues.keys()) { - const { variantGlobs } = - fillDefaultOptionalValues.mock.results[index].value; + for (const { variantGlobs } of pointCutsValues.keys()) { expect(getVariantPaths).toHaveBeenCalledWith({ variantGlobs, appRoot, @@ -55,13 +40,10 @@ describe("processPointCuts", () => { it("should process the variant files, and keep a shared record of config files found between each point cut", () => { for (const [index, pointCut] of pointCutsValues.entries()) { const variantPaths = getVariantPaths.mock.results[index].value; - const { joinPointResolver } = - fillDefaultOptionalValues.mock.results[index].value; expect(processVariantFiles).toHaveBeenCalledWith({ variantPaths, joinPointFiles, pointCut, - joinPointResolver, warnings, configFiles: expect.any(Map), fileSystem, diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js index 8d8e09a..33e4b5d 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js @@ -1,17 +1,20 @@ import { posix } from "path"; -import isJoinPointInvalid from "./isJoinPointInvalid"; +import isJoinPointInvalid from "./isJoinPointInvalid.js"; +import linkJoinPoints from "./linkJoinPoints.js"; const { parse, relative } = posix; +const normalizeToRelativePath = (path, joinDirectory) => + relative(joinDirectory, path).replace(/^([^./])/, "./$1"); + const processVariantFiles = async ({ variantPaths, joinPointFiles, pointCut, - joinPointResolver, warnings, ...rest }) => { for (const variantPath of variantPaths) { - const joinPointPath = joinPointResolver(variantPath); + const joinPointPath = pointCut.joinPointResolver(variantPath); const { dir: directory, base: filename } = parse(joinPointPath); if (!joinPointFiles.has(joinPointPath)) { @@ -27,7 +30,7 @@ const processVariantFiles = async ({ } joinPointFiles.set(joinPointPath, { pointCut, - variants: [] + variantPathMap: new Map() }); } @@ -39,10 +42,11 @@ const processVariantFiles = async ({ continue; } - joinPointFile.variants.push( - relative(directory, variantPath).replace(/^([^./])/, "./$1") - ); + const key = normalizeToRelativePath(variantPath, directory); + joinPointFile.variantPathMap.set(key, variantPath); } + + linkJoinPoints(joinPointFiles); }; export default processVariantFiles; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js index 1ff96c5..4a2a4c2 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js @@ -1,41 +1,37 @@ -import processVariantFiles from "."; -import { memfs } from "memfs"; import { posix } from "path"; +import isJoinPointInvalid from "./isJoinPointInvalid.js"; +import linkJoinPoints from "./linkJoinPoints.js"; +import processVariantFiles from "./index.js"; + const { resolve, join, sep } = posix; +jest.mock("./linkJoinPoints.js", () => jest.fn()); +jest.mock("./isJoinPointInvalid.js", () => jest.fn()); + describe("processVariantFiles", () => { let joinPointFiles; - const pointCut = { name: "test-point-cut" }; - const joinPointResolver = jest.fn(); + const pointCut = { name: "test-point-cut", joinPointResolver: jest.fn() }; let warnings; const variantFileGlob = "test-variant-*.*"; - const variantGlobs = [`/${variantFileGlob}`]; - const appRoot = "/test-app-root/"; const moduleFile = "test-module.js"; const joinPointFolder = "test-folder"; const joinPointPath = join(joinPointFolder, moduleFile); - const { fs: fileSystem } = memfs({ - [`${appRoot}${joinPointPath}`]: "join point" - }); + const rest = { [Symbol("test-key")]: Symbol("test-value") }; - beforeEach(() => { + beforeEach(async () => { + jest.clearAllMocks(); warnings = []; joinPointFiles = new Map(); }); - const act = async ({ variantPaths, configFiles }) => { + const act = async ({ variantPaths }) => { await processVariantFiles({ variantPaths, - configFiles, joinPointFiles, pointCut, - joinPointResolver, - variantGlobs, warnings, - name: moduleFile, - fileSystem, - appRoot + ...rest }); }; @@ -61,14 +57,41 @@ describe("processVariantFiles", () => { `( "when given a variant path ($variantPath)", ({ variantPath, expectedVariant }) => { + const path = resolve(joinPointFolder, variantPath); const variantPaths = new Set([resolve(joinPointFolder, variantPath)]); - describe("when given a variant file that has no matching join point file", () => { + const makeCommonAssertions = () => { + it("should call the joinPointResolver with the path to the variant file", () => { + expect(pointCut.joinPointResolver).toHaveBeenCalledWith(path); + }); + + it("should call linkJoinPoints with the updated joinPointFiles", () => { + expect(linkJoinPoints).toHaveBeenCalledWith(joinPointFiles); + }); + }; + + describe("when given a variant file that is not valid according to the configured config files", () => { + const filename = "test-not-matching-control"; + const joinPointPath = join(joinPointFolder, filename); + beforeEach(async () => { - joinPointResolver.mockReturnValue( - join(joinPointFolder, "test-not-matching-control") - ); - await act({ variantPaths, configFiles: new Map() }); + pointCut.joinPointResolver.mockReturnValue(joinPointPath); + isJoinPointInvalid.mockReturnValue(true); + await act({ + variantPaths, + configFiles: new Map() + }); + }); + + makeCommonAssertions(); + + it("should call isJoinPointInvalid with the expected arguments", () => { + expect(isJoinPointInvalid).toHaveBeenCalledWith({ + filename, + joinPointPath, + directory: joinPointFolder, + ...rest + }); }); it("should add no warnings, and not modify joinPointFiles", async () => { @@ -79,41 +102,27 @@ describe("processVariantFiles", () => { describe("when given a variant file that has a matching join point file", () => { beforeEach(async () => { - joinPointResolver.mockReturnValue(joinPointPath); + pointCut.joinPointResolver.mockReturnValue(joinPointPath); }); describe("and no config file precludes it being valid", () => { beforeEach(async () => { - await act({ variantPaths, configFiles: new Map() }); + isJoinPointInvalid.mockReturnValue(false); + await act({ variantPaths }); }); - it("should add no warnings, and add a single joinPointFile representing the matched join point", async () => { - expect(warnings).toEqual([]); - expect(joinPointFiles).toEqual( - new Map([ - [ - joinPointPath, - { - pointCut, - variants: [expectedVariant] - } - ] - ]) - ); - }); - }); + makeCommonAssertions(); - describe("and a config file confirms it as valid", () => { - beforeEach(async () => { - await act({ - variantPaths, - configFiles: new Map([ - [joinPointFolder, { joinPoints: [moduleFile] }] - ]) + it("should call isJoinPointInvalid with the expected arguments", () => { + expect(isJoinPointInvalid).toHaveBeenCalledWith({ + filename: moduleFile, + joinPointPath, + directory: joinPointFolder, + ...rest }); }); - it("should add no warnings, and add a single joinPointFile representing the matched join point", async () => { + it("should add no warnings, and add a single joinPointFile representing the matched join point, relative to the control module", async () => { expect(warnings).toEqual([]); expect(joinPointFiles).toEqual( new Map([ @@ -121,7 +130,7 @@ describe("processVariantFiles", () => { joinPointPath, { pointCut, - variants: [expectedVariant] + variantPathMap: new Map([[expectedVariant, path]]) } ] ]) @@ -129,20 +138,6 @@ describe("processVariantFiles", () => { }); }); - describe("and a config file precludes it from being valid", () => { - beforeEach(async () => { - await act({ - variantPaths, - configFiles: new Map([[joinPointFolder, { joinPoints: [] }]]) - }); - }); - - it("should add no warnings, and not modify joinPointFiles", async () => { - expect(warnings).toEqual([]); - expect(joinPointFiles).toEqual(new Map()); - }); - }); - describe("and a preceding point cut already identified the join point", () => { const testOtherPointCut = { name: "test-other-point-cut" }; beforeEach(async () => { @@ -150,11 +145,16 @@ describe("processVariantFiles", () => { pointCut: testOtherPointCut }); await act({ - variantPaths, - configFiles: new Map() + variantPaths }); }); + makeCommonAssertions(); + + it("should not check if the join point is invalid again", () => { + expect(isJoinPointInvalid).not.toHaveBeenCalled(); + }); + it("should add a warning, and not modify joinPointFiles", async () => { expect(warnings).toEqual([ `Join point "${joinPointPath}" is already assigned to point cut "${testOtherPointCut.name}". Skipping assignment to "${pointCut.name}".` diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.js new file mode 100644 index 0000000..4b6dd65 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.js @@ -0,0 +1,13 @@ +import { JOIN_POINTS, SCHEME } from "../../constants.js"; + +const linkJoinPoints = (joinPointFiles) => { + for (const [, { variantPathMap }] of joinPointFiles) { + for (const [key, path] of variantPathMap) { + if (joinPointFiles.has(path)) { + variantPathMap.set(key, `${SCHEME}:${JOIN_POINTS}:${path}`); + } + } + } +}; + +export default linkJoinPoints; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.test.js new file mode 100644 index 0000000..ad8b4ff --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.test.js @@ -0,0 +1,109 @@ +import linkJoinPoints from "./linkJoinPoints"; +import { JOIN_POINTS, SCHEME } from "../../constants.js"; + +jest.mock("../../constants.js", () => ({ + JOIN_POINTS: "mockedJoinPoints", + SCHEME: "mockedScheme" +})); + +describe("linkJoinPoints", () => { + let mockJoinPoint1, mockJoinPoint2; + beforeEach(() => { + mockJoinPoint1 = [ + "path/to/joinPoint1", + { + variantPathMap: new Map([ + ["path/to/jointPoint1/variant1", "path/to/jointPoint1/variant1"], + ["path/to/jointPoint1/variant2", "path/to/jointPoint1/variant2"] + ]) + } + ]; + mockJoinPoint2 = [ + "path/to/joinPoint2", + { + variantPathMap: new Map([ + ["path/to/jointPoint2/variant1", "path/to/jointPoint2/variant1"], + ["path/to/jointPoint2/variant2", "path/to/jointPoint2/variant2"] + ]) + } + ]; + }); + + describe("when join points are not chained", () => { + it("should not modify the join point files", () => { + const joinPointFiles = new Map([mockJoinPoint1, mockJoinPoint2]); + linkJoinPoints(joinPointFiles); + + expect(joinPointFiles).toEqual( + new Map([ + [ + "path/to/joinPoint1", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint1/variant1", + "path/to/jointPoint1/variant1" + ], + ["path/to/jointPoint1/variant2", "path/to/jointPoint1/variant2"] + ]) + } + ], + [ + "path/to/joinPoint2", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint2/variant1", + "path/to/jointPoint2/variant1" + ], + ["path/to/jointPoint2/variant2", "path/to/jointPoint2/variant2"] + ]) + } + ] + ]) + ); + }); + }); + + describe("when a variant of a join point is itself a join point", () => { + it("should link variants of the join point to the connected join point", () => { + const connectedJoinPoint2 = [ + "path/to/jointPoint1/variant1", + mockJoinPoint2[1] + ]; + + const joinPointFiles = new Map([mockJoinPoint1, connectedJoinPoint2]); + + linkJoinPoints(joinPointFiles); + + expect(joinPointFiles).toEqual( + new Map([ + [ + "path/to/joinPoint1", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint1/variant1", + `${SCHEME}:${JOIN_POINTS}:path/to/jointPoint1/variant1` + ], + ["path/to/jointPoint1/variant2", "path/to/jointPoint1/variant2"] + ]) + } + ], + [ + "path/to/jointPoint1/variant1", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint2/variant1", + "path/to/jointPoint2/variant1" + ], + ["path/to/jointPoint2/variant2", "path/to/jointPoint2/variant2"] + ]) + } + ] + ]) + ); + }); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js index 83e8d3b..d6a9868 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js @@ -4,12 +4,18 @@ const resourceProxyExistsInRequestChain = ({ proxyResource }) => { const queue = [issuerModule]; + const visited = new WeakSet(); while (queue.length) { const node = queue.shift(); if (node.resource === proxyResource) { return true; } + if (visited.has(node)) { + continue; + } + visited.add(node); + const incomingConnections = moduleGraph.getIncomingConnections(node); queue.push( ...new Set( diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js index 058a137..de74b7a 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js @@ -1,13 +1,14 @@ import resourceProxyExistsInRequestChain from "./resourceProxyExistsInRequestChain"; import { createMockGraph } from "../../../../../test/test-utils"; -const moduleGraph = { getIncomingConnections: jest.fn() }; const proxyResource = Symbol("test-proxy-resource"); describe("resourceProxyExistsInRequestChain", () => { let result; describe("when the issuer module is the proxy resource", () => { + const moduleGraph = { getIncomingConnections: jest.fn() }; + beforeEach(() => { result = resourceProxyExistsInRequestChain({ moduleGraph, @@ -118,28 +119,54 @@ describe("resourceProxyExistsInRequestChain", () => { }); describe("and one of the modules that imported the issuer module was not imported by the proxy resource", () => { - let calls; - - beforeEach(() => { - result = resourceProxyExistsInRequestChain({ - moduleGraph, - issuerModule, - proxyResource + describe("and there is a circular dependency", () => { + beforeEach(() => { + let count = 0; + const newModuleGraph = { + ...moduleGraph, + getIncomingConnections: jest.fn().mockImplementation((module) => { + if (count++ === 10) { + count = 0; + return moduleGraph.getIncomingConnections(issuerModule); + } + return moduleGraph.getIncomingConnections(module); + }) + }; + result = resourceProxyExistsInRequestChain({ + moduleGraph: newModuleGraph, + issuerModule, + proxyResource + }); }); - calls = moduleGraph.getIncomingConnections.mock.calls; + it("should return false, without locking up / running forever", () => { + expect(result).toBe(false); + }); }); - it("should have traversed the whole import tree of the issuer module", () => { - let expectedCount = 1; - for (let level = 1; level <= depth; level++) { - expectedCount += Math.pow(siblingsAtEachDepthCount, level); - } - expect(calls.length).toEqual(expectedCount); - }); + describe("and there is no circular dependency", () => { + beforeEach(() => { + result = resourceProxyExistsInRequestChain({ + moduleGraph, + issuerModule, + proxyResource + }); + }); + + it("should have traversed the whole import tree of the issuer module", () => { + let expectedCount = 1; + for (let level = 1; level <= depth; level++) { + expectedCount += Math.pow(siblingsAtEachDepthCount, level); + } - it("should return false", () => { - expect(result).toBe(false); + expect( + moduleGraph.getIncomingConnections.mock.calls.length + ).toEqual(expectedCount); + }); + + it("should return false", () => { + expect(result).toBe(false); + }); }); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js index 9f36121..21f3fb8 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js @@ -1,11 +1,41 @@ import { PLUGIN_NAME } from "../constants"; -import { posix, win32 } from "path"; +import { sep, posix } from "path"; import { promisify } from "util"; import handleJoinPointMatch from "./handleJoinPointMatch"; const { relative } = posix; -const isLoaderlessFileRequest = (request) => - [".", "/"].includes(request.at(0)) && !request.includes("!"); +const matchJoinPointIfResolved = async ({ + enhancedResolve, + resolveData, + appRoot, + joinPointFiles, + compilation +}) => { + let resolved; + try { + resolved = await enhancedResolve( + {}, + resolveData.context, + resolveData.request, + {} + ); + } catch { + return; + } + + if (!resolved) { + return; + } + + const resource = `/${relative(appRoot, resolved.replaceAll(sep, posix.sep))}`; + if (joinPointFiles.has(resource)) { + handleJoinPointMatch({ + resource, + compilation, + resolveData + }); + } +}; const resolveJoinPoints = ({ compilation, @@ -23,29 +53,18 @@ const resolveJoinPoints = ({ async (resolveData) => { if ( !joinPointFiles.size || - !resolveData.context - .replaceAll(win32.sep, posix.sep) - .startsWith(appRoot) || - !isLoaderlessFileRequest(resolveData.request) + !resolveData.context.replaceAll(sep, posix.sep).startsWith(appRoot) ) { return; } - const resolved = await enhancedResolve( - {}, - resolveData.context, - resolveData.request, - {} - ); - - const resource = `/${relative(appRoot, resolved.replaceAll(win32.sep, posix.sep))}`; - if (joinPointFiles.has(resource)) { - handleJoinPointMatch({ - resource, - compilation, - resolveData - }); - } + await matchJoinPointIfResolved({ + appRoot, + joinPointFiles, + enhancedResolve, + resolveData, + compilation + }); } ); }; diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js index e9628c8..36a9c81 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js @@ -1,7 +1,7 @@ import { PLUGIN_NAME } from "../constants"; import handleJoinPointMatch from "./handleJoinPointMatch"; import { sep, join } from "path"; -import resolvePointCuts from "."; +import resolveJoinPoints from "."; jest.mock("../constants", () => ({ PLUGIN_NAME: "test-plugin-name" @@ -48,7 +48,7 @@ describe("resolveJoinPoints", () => { const joinPointFiles = new Map(); beforeEach(() => { - resolvePointCuts({ + resolveJoinPoints({ compilation, appRoot, normalModuleFactory, @@ -64,7 +64,7 @@ describe("resolveJoinPoints", () => { beforeEach(() => { [, beforeResolveCallback] = normalModuleFactory.hooks.beforeResolve.tapPromise.mock.lastCall; - handleJoinPointMatch.mockClear(); + jest.clearAllMocks(); beforeResolveCallback(); }); @@ -77,7 +77,7 @@ describe("resolveJoinPoints", () => { describe("when there are some join points previously identified", () => { const joinPointFile = "/test-folder/test-join-point-file"; beforeEach(() => { - resolvePointCuts({ + resolveJoinPoints({ compilation, appRoot, normalModuleFactory, @@ -109,53 +109,53 @@ describe("resolveJoinPoints", () => { }); }); - describe("and the request is for a file with a webpack loader", () => { - beforeEach(() => { - resolveData = { - context: appRoot.replaceAll("/", sep), - request: "test-loader!test-request" - }; - beforeResolveCallback(resolveData); + const makeCommonAssertions = () => { + it("should resolve the request to a module file", () => { + expect(mockResolve).toHaveBeenCalledWith( + {}, + resolveData.context, + resolveData.request, + {}, + expect.any(Function) + ); }); + }; - it("should not try to handle a match", () => { - expect(handleJoinPointMatch).not.toHaveBeenCalled(); - }); - }); - - describe("and the request is for a module rather than a file", () => { + describe("and the request is for a file within the app root", () => { beforeEach(() => { resolveData = { context: appRoot.replaceAll("/", sep), - request: "test-request" + request: "/test-request" }; - beforeResolveCallback(resolveData); }); - it("should not try to handle a match", () => { - expect(handleJoinPointMatch).not.toHaveBeenCalled(); - }); - }); + describe("and an error is thrown whilst trying to resolve", () => { + beforeEach(() => { + mockResolve.mockImplementationOnce(() => { + throw new Error("Test error"); + }); + beforeResolveCallback(resolveData); + }); - describe("and the request is for a file within the app root, without any loaders", () => { - beforeEach(() => { - resolveData = { - context: appRoot.replaceAll("/", sep), - request: "/test-request" - }; + makeCommonAssertions(); + + it("should not try to handle a match", () => { + expect(handleJoinPointMatch).not.toHaveBeenCalled(); + }); }); - const makeCommonAssertions = () => { - it("should resolve the request to a module file", () => { - expect(mockResolve).toHaveBeenCalledWith( - {}, - resolveData.context, - resolveData.request, - {}, - expect.any(Function) - ); + describe("and the file cannot be resolved", () => { + beforeEach(() => { + mockResolvedResource = false; + beforeResolveCallback(resolveData); }); - }; + + makeCommonAssertions(); + + it("should not try to handle a match", () => { + expect(handleJoinPointMatch).not.toHaveBeenCalled(); + }); + }); describe("and the file is not a join point", () => { beforeEach(() => { diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js deleted file mode 100644 index cab2b71..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js +++ /dev/null @@ -1,20 +0,0 @@ -import { posix } from "path"; -import regexgen from "regexgen"; -import { POINT_CUTS, SCHEME } from "../constants.js"; -const { dirname } = posix; - -const generateJoinPoint = ({ joinPointFiles, path }) => { - const { - pointCut: { name }, - variants - } = joinPointFiles.get(path); - const directory = dirname(path); - const regex = regexgen(variants); - - return `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}"; -import * as joinPoint from "${path}"; -const variants = import.meta.webpackContext("${directory}", { recursive: true, regExp: ${regex} }); -export default pointCut({ joinPoint, variants });`; -}; - -export default generateJoinPoint; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js deleted file mode 100644 index d03a273..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import { POINT_CUTS, SCHEME } from "../constants.js"; -import generateJoinPoint from "./generateJoinPoint.js"; - -jest.mock("../constants", () => ({ - SCHEME: "test-scheme", - POINT_CUTS: "test-point-cuts" -})); - -describe("generateJoinPoint", () => { - const path = "test-folder/test-path"; - const pointCutName = "test-point-cut"; - const variants = [ - "test-sub-folder/test-variant-1", - "test-sub-folder/test-variant-2", - "test-other-sub-folder/test-variant-1" - ]; - let result; - - beforeEach(() => { - const joinPointFiles = new Map([ - [path, { pointCut: { name: pointCutName }, variants }] - ]); - result = generateJoinPoint({ joinPointFiles, path }); - }); - - it("should return a script that imports the appropriate point cut for the join point", () => { - expect(result).toMatch( - `import pointCut from "${SCHEME}:${POINT_CUTS}:/${pointCutName}";` - ); - }); - - it("should return a script that imports the base / control module for the join point", () => { - expect(result).toMatch(`import * as joinPoint from "${path}";`); - }); - - it("should return a script that imports all the valid variants of the base / control module into a webpackContext, using a minimal regex that matches all the variants", () => { - expect(result).toMatch( - `const variants = import.meta.webpackContext("${ - path.split("/")[0] - }", { recursive: true, regExp: /test\\-(?:other\\-sub\\-folder\\/test\\-variant\\-1|sub\\-folder\\/test\\-variant\\-[12])/ });` - ); - }); - - it("should return a script exports a default export which calls the point cut, passing the join point (control module) and the variants", () => { - expect(result).toMatch("export default pointCut({ joinPoint, variants });"); - }); -}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.js new file mode 100644 index 0000000..770367f --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.js @@ -0,0 +1,5 @@ +const createVariantPathMap = (content) => `const variantPathMap = new Map([ +${content} +]);`; + +export default createVariantPathMap; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.test.js new file mode 100644 index 0000000..cabf55a --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.test.js @@ -0,0 +1,11 @@ +import createVariantPathMap from "./createVariantPathMap"; + +describe("createVariantPathMap", () => { + it("should return code to create a variantPathMap constant, wrapping the supplied content in a Map", () => { + const testContent = "test-content"; + expect(createVariantPathMap(testContent)) + .toEqual(`const variantPathMap = new Map([ +${testContent} +]);`); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.js new file mode 100644 index 0000000..49a6583 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.js @@ -0,0 +1,15 @@ +import createVariantPathMap from "./createVariantPathMap.js"; + +const importCodeGenerator = ({ joinPointPath, variantPathMap }) => { + const variantsKeys = Array.from(variantPathMap.keys()); + return `import * as joinPoint from "${joinPointPath}"; +${variantsKeys + .map( + (key, index) => + `import * as variant_${index} from "${variantPathMap.get(key)}";` + ) + .join("\n")} +${createVariantPathMap(variantsKeys.map((key, index) => ` ["${key}", variant_${index}]`).join(",\n"))}`; +}; + +export default importCodeGenerator; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.test.js new file mode 100644 index 0000000..f80d411 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.test.js @@ -0,0 +1,44 @@ +import importCodeGenerator from "./importCodeGenerator"; + +describe("importCodeGenerator", () => { + const joinPointPath = "/test-folder/test-path"; + const relativePaths = [ + "/test-sub-folder/test-variant-1", + "/test-sub-folder/test-variant-2", + "/test-other-sub-folder/test-variant-1" + ]; + const variantPathMap = new Map( + relativePaths.map((relativePath) => [ + relativePath, + `${joinPointPath}${relativePath}` + ]) + ); + + let result; + + beforeEach(() => { + result = importCodeGenerator({ + joinPointPath, + variantPathMap + }); + }); + + it("should return code that imports the base / control module for the join point", () => { + expect(result).toMatch(`import * as joinPoint from "${joinPointPath}";`); + }); + + it("should return code that imports all the valid variants of the base / control module, storing in variables", () => { + expect(result).toMatch(` +import * as variant_0 from "${joinPointPath}${relativePaths[0]}"; +import * as variant_1 from "${joinPointPath}${relativePaths[1]}"; +import * as variant_2 from "${joinPointPath}${relativePaths[2]}";`); + }); + + it("should return code that creates a Map of variants, keyed by relative path, valued as the variant module", () => { + expect(result).toMatch(`const variantPathMap = new Map([ + ["/test-sub-folder/test-variant-1", variant_0], + ["/test-sub-folder/test-variant-2", variant_1], + ["/test-other-sub-folder/test-variant-1", variant_2] +]);`); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.js new file mode 100644 index 0000000..58b7ea6 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.js @@ -0,0 +1,18 @@ +import { POINT_CUTS, SCHEME } from "../../constants.js"; +import importCodeGenerator from "./importCodeGenerator.js"; + +const generateJoinPoint = ({ joinPointFiles, joinPointPath }) => { + const { + pointCut: { name }, + variantPathMap + } = joinPointFiles.get(joinPointPath); + const pointCutImport = `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}";`; + + const code = importCodeGenerator({ joinPointPath, variantPathMap }); + + return `${pointCutImport} +${code} +export default pointCut({ joinPoint, variantPathMap });`; +}; + +export default generateJoinPoint; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.test.js new file mode 100644 index 0000000..3a114d8 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.test.js @@ -0,0 +1,51 @@ +import { POINT_CUTS, SCHEME } from "../../constants.js"; +import generateJoinPoint from "./index.js"; +import importCodeGenerator from "./importCodeGenerator.js"; + +jest.mock("../../constants", () => ({ + SCHEME: "test-scheme", + POINT_CUTS: "test-point-cuts" +})); +const mockImportCode = + "const joinPoint = 'test-join-point'; const variantPathMap = 'test-variants';"; +jest.mock("./importCodeGenerator.js", () => jest.fn(() => mockImportCode)); + +describe("generateJoinPoint", () => { + const joinPointPath = "/test-path"; + const pointCutName = "test-point-cut"; + const variantPathMap = Symbol("test-variant-path-map"); + const pointCut = { + name: pointCutName + }; + let result; + + beforeEach(() => { + const joinPointFiles = new Map([ + [joinPointPath, { pointCut, variantPathMap }] + ]); + result = generateJoinPoint({ + joinPointFiles, + joinPointPath + }); + }); + + it("should return a script that imports the appropriate point cut for the join point", () => { + expect(result).toMatch( + `import pointCut from "${SCHEME}:${POINT_CUTS}:/${pointCutName}";` + ); + }); + + it("should call the import code generator of the passed loading strategy, and return a script that includes the import code", () => { + expect(importCodeGenerator).toHaveBeenCalledWith({ + joinPointPath, + variantPathMap + }); + expect(result).toMatch(mockImportCode); + }); + + it("should return a script that exports a default export which calls the point cut, passing the join point (control module) and the variantPathMap returned by the import code", () => { + expect(result).toMatch( + "export default pointCut({ joinPoint, variantPathMap });" + ); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js index bb104dc..4361c83 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js @@ -1,5 +1,5 @@ -const generatePointCut = ({ pointCuts, path }) => { - const pointCutName = path.slice(1); +const generatePointCut = ({ pointCuts, joinPointPath }) => { + const pointCutName = joinPointPath.slice(1); const { togglePointModule, toggleHandler = "@asos/web-toggle-point-webpack/pathSegmentToggleHandler" diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js index cbcc7d6..f825a7c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js @@ -2,7 +2,7 @@ import generatePointCut from "./generatePointCut.js"; describe("generatePointCut", () => { const pointCutName = "test-point-cut"; - const path = `/${pointCutName}`; + const joinPointPath = `/${pointCutName}`; const togglePointModule = "test-toggle-point-path"; let result, pointCuts; @@ -30,7 +30,7 @@ describe("generatePointCut", () => { beforeEach(() => { pointCuts[1].toggleHandler = toggleHandler; - result = generatePointCut({ pointCuts, path }); + result = generatePointCut({ pointCuts, joinPointPath }); }); it("should return a script that imports the appropriate toggle handler", () => { @@ -42,7 +42,7 @@ describe("generatePointCut", () => { describe("when a toggle handler is not configured against the point cut", () => { beforeEach(() => { - result = generatePointCut({ pointCuts, path }); + result = generatePointCut({ pointCuts, joinPointPath }); }); it("should return a script that imports the default toggle handler (a path segment toggle handler)", () => { diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js index b5d014f..db42a9c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js @@ -1,5 +1,5 @@ import { PLUGIN_NAME, POINT_CUTS, JOIN_POINTS, SCHEME } from "../constants.js"; -import generateJoinPoint from "./generateJoinPoint.js"; +import generateJoinPoint from "./generateJoinPoint/index.js"; import generatePointCut from "./generatePointCut.js"; const setupSchemeModules = ({ @@ -11,13 +11,13 @@ const setupSchemeModules = ({ NormalModule.getCompilationHooks(compilation) .readResource.for(SCHEME) .tap(PLUGIN_NAME, ({ resourcePath }) => { - const [, type, path] = resourcePath.split(/:(.*?):(.*)/, 3); + const [, type, joinPointPath] = resourcePath.split(/:(.*?):(.*)/, 3); switch (type) { case POINT_CUTS: { - return generatePointCut({ pointCuts, path }); + return generatePointCut({ pointCuts, joinPointPath }); } case JOIN_POINTS: { - return generateJoinPoint({ joinPointFiles, path }); + return generateJoinPoint({ joinPointFiles, joinPointPath }); } } }); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js index ca8a234..da55147 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js @@ -1,5 +1,5 @@ import { PLUGIN_NAME, POINT_CUTS, JOIN_POINTS, SCHEME } from "../constants.js"; -import generateJoinPoint from "./generateJoinPoint.js"; +import generateJoinPoint from "./generateJoinPoint/index.js"; import generatePointCut from "./generatePointCut.js"; import setupSchemeModules from "./index.js"; @@ -46,35 +46,42 @@ describe("setupSchemeModules", () => { let result; describe("and the resource is prefixed with the point cuts type", () => { - const path = "test-point-cut-name"; + const joinPointPath = "test-point-cut-name"; const output = Symbol("test-output"); beforeEach(() => { const [, callback] = tap.mock.lastCall; generatePointCut.mockReturnValue(output); - result = callback({ resourcePath: `${SCHEME}:${POINT_CUTS}:${path}` }); + result = callback({ + resourcePath: `${SCHEME}:${POINT_CUTS}:${joinPointPath}` + }); }); it("should generate the point cut and return the generated module to the read resource hook", () => { - expect(generatePointCut).toHaveBeenCalledWith({ pointCuts, path }); + expect(generatePointCut).toHaveBeenCalledWith({ + pointCuts, + joinPointPath + }); expect(result).toBe(output); }); }); describe("and the resource is prefixed with the join points type", () => { - const path = "test-path"; + const joinPointPath = "test-path"; const output = Symbol("test-output"); beforeEach(() => { const [, callback] = tap.mock.lastCall; generateJoinPoint.mockReturnValue(output); - result = callback({ resourcePath: `${SCHEME}:${JOIN_POINTS}:${path}` }); + result = callback({ + resourcePath: `${SCHEME}:${JOIN_POINTS}:${joinPointPath}` + }); }); it("should generate a join point and return the generated module to the read resource hook", () => { expect(generateJoinPoint).toHaveBeenCalledWith({ joinPointFiles, - path + joinPointPath }); expect(result).toBe(output); }); diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js index 99a2633..5c568d3 100644 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js +++ b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js @@ -1,9 +1,9 @@ -const buildTree = (map = new Map(), parts, variants, key) => { +const buildTree = (map = new Map(), parts, value) => { const [part, ...rest] = parts; if (rest.length) { - map.set(part, buildTree(map.get(part), rest, variants, key)); + map.set(part, buildTree(map.get(part), rest, value)); } else { - map.set(part, variants(key)); + map.set(part, value); } return map; }; @@ -15,14 +15,18 @@ const buildTree = (map = new Map(), parts, variants, key) => { * @param {object} options plugin options * @param {function} options.togglePoint a method that chooses the appropriate module at runtime * @param {module} options.joinPoint the join point module - * @param {string[]} options.variants an array of paths, as generated by webpack, that point to variants of the join point module + * @param {Map} params.variantPathMap a Map of posix file paths, relative to the join point module, to variant modules * @returns {function} A handler of join points injected by the plugin */ -const pathSegmentToggleHandler = ({ togglePoint, joinPoint, variants }) => { +const pathSegmentToggleHandler = ({ + togglePoint, + joinPoint, + variantPathMap +}) => { let featuresMap; - for (const key of variants.keys()) { + for (const [key, value] of variantPathMap) { const parts = key.split("/").slice(0, -1).slice(2); - featuresMap = buildTree(featuresMap, parts, variants, key); + featuresMap = buildTree(featuresMap, parts, value); } return togglePoint(joinPoint, featuresMap); }; diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js index 6e64730..ecf556f 100644 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js +++ b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js @@ -5,9 +5,7 @@ const togglePoint = jest.fn(() => toggleOutcome); const joinPoint = Symbol("mock-join-point"); describe("pathSegmentToggleHandler", () => { - let result, variantsMap; - const variants = (key) => variantsMap[key]; - variants.keys = () => Object.keys(variantsMap); + let result; beforeEach(() => { jest.clearAllMocks(); @@ -17,15 +15,25 @@ describe("pathSegmentToggleHandler", () => { const keyArray = [...Array(segmentCount).keys()]; describe(`given a list of variant paths with ${segmentCount} path segments (after the variants path)`, () => { + let variantPathMap; + beforeEach(() => { const segments = keyArray.map((key) => `test-segment-${key}/`); - variantsMap = { - [`./__variants__/${segments.join("")}test-variant.js`]: - Symbol("test-variant"), - [`./__variants__/${segments.reverse().join("")}test-variant.js`]: + variantPathMap = new Map([ + [ + `./__variants__/${segments.join("")}test-variant.js`, + Symbol("test-variant") + ], + [ + `./__variants__/${segments.reverse().join("")}test-variant.js`, Symbol("test-variant") - }; - result = pathSegmentToggleHandler({ togglePoint, joinPoint, variants }); + ] + ]); + result = pathSegmentToggleHandler({ + togglePoint, + joinPoint, + variantPathMap + }); }); it("should call the toggle point with the join point module and a map", () => { @@ -44,14 +52,14 @@ describe("pathSegmentToggleHandler", () => { }); it("should return a map containing maps for each segment, concluding with the variant at the leaf node", () => { - for (const key of Object.keys(variantsMap)) { + for (const key of Object.keys(variantPathMap)) { const segments = key.split("/").slice(0, -1); let node = map; for (const segment of segments.slice(2)) { expect(node.has(segment)).toBe(true); node = node.get(segment); } - expect(node).toBe(variantsMap[key]); + expect(node).toBe(variantPathMap.get(key)); } }); }); diff --git a/packages/webpack/test/test-utils.js b/packages/webpack/test/test-utils.js index 7065956..726c018 100644 --- a/packages/webpack/test/test-utils.js +++ b/packages/webpack/test/test-utils.js @@ -20,7 +20,6 @@ export const createMockGraph = ({ depth, siblingsAtEachDepthCount }) => { createGraph(rootNode, depth, siblingsAtEachDepthCount); const getIncomingConnections = jest.fn(function* (module) { - // eslint-disable-next-line jsdoc/require-jsdoc function* traverse(node) { if (node.resource === module.resource) { yield* issuersMap.get(node).map((node) => ({