From d0ce7f1f89e770afda63a54d0992b0e33e1ae427 Mon Sep 17 00:00:00 2001 From: Tom Pereira Date: Tue, 24 Dec 2024 17:31:32 +0000 Subject: [PATCH 01/89] change initial commit message in changelogs (#3) --- docs/CHANGELOG.md | 2 +- packages/features/docs/CHANGELOG.md | 2 +- packages/react-pointcuts/docs/CHANGELOG.md | 2 +- packages/ssr/docs/CHANGELOG.md | 2 +- packages/webpack/docs/CHANGELOG.md | 3 +-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fe59d05..6f9d606 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -233,4 +233,4 @@ This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). ### Added -- Initial version, copying code from PLP. +- Initial commit. diff --git a/packages/features/docs/CHANGELOG.md b/packages/features/docs/CHANGELOG.md index be37a9f..abc3dd8 100644 --- a/packages/features/docs/CHANGELOG.md +++ b/packages/features/docs/CHANGELOG.md @@ -93,4 +93,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial version, copying code from PLP +- Initial commit. diff --git a/packages/react-pointcuts/docs/CHANGELOG.md b/packages/react-pointcuts/docs/CHANGELOG.md index 10c985a..932854d 100644 --- a/packages/react-pointcuts/docs/CHANGELOG.md +++ b/packages/react-pointcuts/docs/CHANGELOG.md @@ -103,4 +103,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial version, copying code from PLP +- Initial commit. diff --git a/packages/ssr/docs/CHANGELOG.md b/packages/ssr/docs/CHANGELOG.md index f7b0824..75ca4ea 100644 --- a/packages/ssr/docs/CHANGELOG.md +++ b/packages/ssr/docs/CHANGELOG.md @@ -83,4 +83,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial version, copying code from PLP +- Initial commit. diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index ae2c30c..5102394 100644 --- a/packages/webpack/docs/CHANGELOG.md +++ b/packages/webpack/docs/CHANGELOG.md @@ -103,5 +103,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial version, copying code from PLP -- Added unit tests around the plugin itself, not originally developed in PLP +- Initial commit From 27e436a05e74b57ec5de78cb88fe09594c55c70e Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:30:26 +0000 Subject: [PATCH 02/89] [#18] Fix JSDoc module names (#19) * rename to proper module namespace * update docs links * update versions * web toggle point in readme title * fixup changelog from revised 0.x range * 2.0.0 -> 0.5.0 in oss version scheme * fix broken link syntax in CHANGELOG * consistent quoting * more version history issues * fixup module name in jsdoc * add web remove sdkInstanceProvider * remove SDKInstanceProvider * fixup jsdoc dedupe * tweak * clarity re: ssr package * casing etc --- docs/CHANGELOG.md | 17 +++++++++----- docs/README.md | 2 +- docs/dedupeExternalJsdocPlugin.js | 10 ++++----- package-lock.json | 12 +++++----- package.json | 2 +- packages/features/docs/CHANGELOG.md | 10 +++++++-- packages/features/docs/README.md | 2 +- packages/features/package.json | 2 +- packages/features/src/global.js | 14 ++++++------ packages/features/src/global/store.js | 10 ++++----- .../features/src/nodeRequestScoped/store.js | 10 ++++----- packages/features/src/reactContext/store.js | 10 ++++----- .../src/ssrBackedReactContext/store.js | 12 +++++----- packages/react-pointcuts/docs/CHANGELOG.md | 8 ++++++- packages/react-pointcuts/docs/README.md | 2 +- packages/react-pointcuts/package.json | 2 +- packages/react-pointcuts/src/external.js | 11 ---------- .../src/getCodeSelectionPlugins.js | 2 +- packages/react-pointcuts/src/index.js | 2 +- .../src/withTogglePointFactory/index.js | 8 +++---- .../src/withToggledHookFactory/index.js | 8 +++---- packages/ssr/docs/CHANGELOG.md | 8 ++++++- packages/ssr/docs/README.md | 2 +- packages/ssr/package.json | 2 +- packages/ssr/src/external.js | 11 ---------- packages/ssr/src/index.js | 4 ++-- .../ssr/src/serializationFactory/index.js | 22 +++++++++---------- packages/ssr/src/withJsonIsomorphism/index.js | 2 +- packages/webpack/docs/CHANGELOG.md | 6 +++++ packages/webpack/docs/README.md | 2 +- packages/webpack/package.json | 2 +- packages/webpack/src/index.js | 2 +- .../src/plugins/togglePointInjection/index.js | 2 +- 33 files changed, 115 insertions(+), 106 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6f9d606..532c7d8 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.10.2] - 2024-12-26 + +### Fixed + +- "Toggle Point" to "Web Toggle Point" in title of `README.md` +- fixed the dedupe external JSDoc plugin, after move to type imports + ## [0.10.1] - 2024-12-24 ### Fixed @@ -73,7 +80,7 @@ This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). ### Fixed -- Removed old `yarn.lock` left over from 1.0.3 update. +- Removed old `yarn.lock` left over from 0.4.3 update. ## Added @@ -85,7 +92,7 @@ This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). - Moved to v4 of [`upload-artifact`](https://github.com/actions/upload-artifact) and [`download-artifact`](https://github.com/actions/download-artifact) actions - Changed nature of pre-release packages to `beta` from `alpha` (better matching the reality of how these pre-releases are used) -- Fixed up contribution guide since version 2.0.0 added the proposed update checks +- Fixed up contribution guide since version 0.5.0 added the proposed update checks - Updated to JSDoc 4, issue with factories resolved ### Changed @@ -127,7 +134,7 @@ This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). ### Fixed -- Fixup documentation left fallow from package split (2.0.0) +- Fixup documentation left fallow from package split (0.5.0) - Upgrade serialize-javascript to 6.0.2 to avoid [`SNYK-JS-SERIALIZEJAVASCRIPT-614760`](https://security.snyk.io/vuln/SNYK-JS-SERIALIZEJAVASCRIPT-6147607) - snyk ignore [`SNYK-JS-INFLIGHT-6095116`](https://security.snyk.io/vuln/SNYK-JS-INFLIGHT-6095116) - move to use asos runner groups @@ -157,7 +164,7 @@ This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). ### Changed - Split the "app" package into separate "ssr", "features" and "react-pointcuts" packages. -- Move to explicit rather than wildcard workspaces, to enable reification of the repo when publishing (waiting on [https://github.com/Roaders/workspace-version/issues/3](an issue to resolve)) +- Move to explicit rather than wildcard workspaces, to enable reification of the repo when publishing (waiting on [an issue to resolve](https://github.com/Roaders/workspace-version/issues/3)) - Updated the `dedupeExternalJsdocPlugin` to de-duplicate members of external namespaces, rather than just the namespaces themselves (to ensure we don't duplicate React, HostApplication etc. in the html documentation) - Updated packages for snyk vulnerabilities, populated policy file - Removed redundant export fields from workspace `package.json` @@ -175,7 +182,7 @@ This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). ### Added -- Danger support, with checks for CHANGELOG.md updates and package-lock.json updates +- Danger support, with checks for `CHANGELOG.md` updates and `package-lock.json` updates ### Fixed diff --git a/docs/README.md b/docs/README.md index 116c4fd..e6e59fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@

-

Toggle Point

+

Web Toggle Point

A library providing a means to toggle or branch web application code. diff --git a/docs/dedupeExternalJsdocPlugin.js b/docs/dedupeExternalJsdocPlugin.js index e339a52..56ee304 100644 --- a/docs/dedupeExternalJsdocPlugin.js +++ b/docs/dedupeExternalJsdocPlugin.js @@ -18,15 +18,15 @@ exports.defineTags = function (dictionary) { .synonym("external"); }; -const seenExternals = new Map(); +const seen = new Map(); exports.handlers = { jsdocCommentFound: function (e) { if (e.filename.endsWith("external.js")) { - const match = e.comment.match(/external:(\S+)/); + const match = e.comment.match(/(?:[\s\S]*@typedef \{.+\} (?.+))?[\s\S]+external:(?\S+)/); if (match) { - const [external] = match; - if (!seenExternals.has(external)) { - seenExternals.set(external, true); + const symbol = match.groups.typedef || match.groups.external; + if (!seen.has(symbol)) { + seen.set(symbol, true); } else { e.comment = "/**/"; } diff --git a/package-lock.json b/package-lock.json index e3b0abd..71ede54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@asos/web-toggle-point", - "version": "0.10.1", + "version": "0.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@asos/web-toggle-point", - "version": "0.10.1", + "version": "0.10.2", "license": "MIT", "workspaces": [ "packages/features", @@ -20346,7 +20346,7 @@ }, "packages/features": { "name": "@asos/web-toggle-point-features", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -20403,7 +20403,7 @@ }, "packages/react-pointcuts": { "name": "@asos/web-toggle-point-react-pointcuts", - "version": "0.4.0", + "version": "0.4.2", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -20441,7 +20441,7 @@ }, "packages/ssr": { "name": "@asos/web-toggle-point-ssr", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -20477,7 +20477,7 @@ }, "packages/webpack": { "name": "@asos/web-toggle-point-webpack", - "version": "0.7.2", + "version": "0.7.3", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", diff --git a/package.json b/package.json index 5c5e40e..d72f35a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@asos/web-toggle-point", - "version": "0.10.1", + "version": "0.10.2", "repository": "git@github.com:asos/web-toggle-point.git", "homepage": "https://asos.github.io/web-toggle-point/", "license": "MIT", diff --git a/packages/features/docs/CHANGELOG.md b/packages/features/docs/CHANGELOG.md index abc3dd8..fa7b6a2 100644 --- a/packages/features/docs/CHANGELOG.md +++ b/packages/features/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] - 2024-12-26 + +### Fixed + +- updated some errant JSDoc namespaces + ## [0.3.0] - 2024-11-28 ### Changed @@ -32,7 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - updated to latest `@testing-library/react` to remove errant warning about import of `act` - updated to `react@18.3.1`, set minimum required react to `17` - - technically a breaking change, but `jsx-runtime` already introduced in [version 1.0.0](#100---2023-09-12)... so was already broken, oops. + - technically a breaking change, but `jsx-runtime` already introduced in [version 0.1.0](#010---2023-09-12)... so was already broken, oops. - renamed commonJs exports to have `.cjs` extension to prevent `[ERR_REQUIRE_ESM]` errors in consumers that aren't `"type": "module"` ## [0.2.2] - 2024-12-26 @@ -61,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixup documentation left fallow from package split (0.1.0) +- Fixup documentation left fallow from package split ([version 0.1.0](#010---2023-09-12)) ## [0.1.1] - 2023-11-16 diff --git a/packages/features/docs/README.md b/packages/features/docs/README.md index 0353466..fd7cdef 100644 --- a/packages/features/docs/README.md +++ b/packages/features/docs/README.md @@ -8,7 +8,7 @@ A store should be chosen based on the requirement for global or partitioned stat ## Usage -See: [JSDoc output](https://asos.github.io/web-toggle-point/module-asos-web-toggle-point-features.html) +See: [JSDoc output](https://asos.github.io/web-toggle-point/module-web-toggle-point-features.html) > [!WARNING] > ### Use with React 17 diff --git a/packages/features/package.json b/packages/features/package.json index 36abbcf..e38f533 100644 --- a/packages/features/package.json +++ b/packages/features/package.json @@ -1,7 +1,7 @@ { "name": "@asos/web-toggle-point-features", "description": "toggle point features code", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "type": "module", "main": "./lib/main.es5.cjs", diff --git a/packages/features/src/global.js b/packages/features/src/global.js index abf4590..bd618e8 100644 --- a/packages/features/src/global.js +++ b/packages/features/src/global.js @@ -2,21 +2,21 @@ import "./external"; /** * Application code for holding feature toggle state - * @module toggle-point-features + * @module web-toggle-point-features */ /** * Interface for feature toggle stores * * @interface FeaturesStore - * @memberof module:toggle-point-features + * @memberof module:web-toggle-point-features */ /** * Method to return the value of the feature toggle store. * For {@link https://reactjs.org/docs/context.html|React context}-backed feature stores, this should be called following {@link https://react.dev/warnings/invalid-hook-call-warning|the rules of hooks} * * @function - * @memberof module:toggle-point-features + * @memberof module:web-toggle-point-features * @name FeaturesStore#getFeatures */ @@ -24,13 +24,13 @@ import "./external"; * Interface for singleton value-based feature toggle stores * * @interface SingletonFeaturesStore - * @memberof module:toggle-point-features + * @memberof module:web-toggle-point-features */ /** * Method to set a value to the feature toggle store. * * @function - * @memberof module:toggle-point-features + * @memberof module:web-toggle-point-features * @name SingletonFeaturesStore#useValue * @param {object} params parameters * @param {object} params.value A value to store, used to drive feature toggles. @@ -40,13 +40,13 @@ import "./external"; * Interface for {@link https://reactjs.org/docs/context.html|React context}-based feature toggle stores * * @interface ContextFeaturesStore - * @memberof module:toggle-point-features + * @memberof module:web-toggle-point-features */ /** * Method to create a React context provider, linked to the features store. * * @function - * @memberof module:toggle-point-features + * @memberof module:web-toggle-point-features * @name ContextFeaturesStore#providerFactory * @returns {external:React.Component} A react context provider that accepts a `value` prop, representing the feature toggle state. */ diff --git a/packages/features/src/global/store.js b/packages/features/src/global/store.js index 7722bd3..2a0c8da 100644 --- a/packages/features/src/global/store.js +++ b/packages/features/src/global/store.js @@ -8,16 +8,16 @@ const storeMap = new WeakMap(); * A thin wrapper around a singleton, used as an extension point for future plugins. * Consider {@link https://github.com/christophehurpeau/deep-freeze-es6|deep freezing} the value to prevent accidental mutation, if this is intended to be static. * For reactive decisions, consider implementing something that allows for reactivity e.g. a {@link https://github.com/pmndrs/valtio|valtio/vanilla} proxy, and subscribe appropriately in a toggle point. - * @memberof module:toggle-point-features - * @returns {module:toggle-point-features.globalFeaturesStore} A store for features, held globally in the application. + * @memberof module:web-toggle-point-features + * @returns {module:web-toggle-point-features.globalFeaturesStore} A store for features, held globally in the application. */ const globalFeaturesStoreFactory = () => { const identifier = Symbol(); /** * @name globalFeaturesStore - * @memberof module:toggle-point-features - * @implements module:toggle-point-features.FeaturesStore - * @implements module:toggle-point-features.SingletonFeaturesStore + * @memberof module:web-toggle-point-features + * @implements module:web-toggle-point-features.FeaturesStore + * @implements module:web-toggle-point-features.SingletonFeaturesStore */ return { useValue: ({ value }) => { diff --git a/packages/features/src/nodeRequestScoped/store.js b/packages/features/src/nodeRequestScoped/store.js index 0092fb4..c2232df 100644 --- a/packages/features/src/nodeRequestScoped/store.js +++ b/packages/features/src/nodeRequestScoped/store.js @@ -4,17 +4,17 @@ import { AsyncLocalStorage } from "async_hooks"; * A factory function used to create a store for features, held in request-scoped global value. * Should only be used server-side, for anything user or request specific. * A thin wrapper around node {@link https://nodejs.org/api/async_context.html#class-asynclocalstorage|AsyncLocalStorage}, used as an extension point for future plugins. - * @memberof module:toggle-point-features - * @returns {module:toggle-point-features.requestScopedFeaturesStore} A store for features, scoped for the current request. + * @memberof module:web-toggle-point-features + * @returns {module:web-toggle-point-features.requestScopedFeaturesStore} A store for features, scoped for the current request. */ const requestScopedFeaturesStoreFactory = () => { const store = new AsyncLocalStorage(); /** * @name requestScopedFeaturesStore - * @memberof module:toggle-point-features - * @implements module:toggle-point-features.FeaturesStore - * @implements module:toggle-point-features.SingletonFeaturesStore + * @memberof module:web-toggle-point-features + * @implements module:web-toggle-point-features.FeaturesStore + * @implements module:web-toggle-point-features.SingletonFeaturesStore */ return { useValue: ({ value, scopeCallBack }) => { diff --git a/packages/features/src/reactContext/store.js b/packages/features/src/reactContext/store.js index db64c55..6ef45cb 100644 --- a/packages/features/src/reactContext/store.js +++ b/packages/features/src/reactContext/store.js @@ -4,17 +4,17 @@ import PropTypes from "prop-types"; /** * A factory function used to create a store for features, held in a {@link https://reactjs.org/docs/context.html|React context}. * A thin wrapper around a context, used as an extension point for future plugins. - * @memberof module:toggle-point-features - * @returns {module:toggle-point-features.reactContextFeaturesStore} A store for features, held within a {@link https://reactjs.org/docs/context.html|React context}. + * @memberof module:web-toggle-point-features + * @returns {module:web-toggle-point-features.reactContextFeaturesStore} A store for features, held within a {@link https://reactjs.org/docs/context.html|React context}. */ const reactContextFeaturesStoreFactory = ({ name }) => { const context = createContext(); /** * @name reactContextFeaturesStore - * @memberof module:toggle-point-features - * @implements module:toggle-point-features.FeaturesStore - * @implements module:toggle-point-features.ContextFeaturesStore + * @memberof module:web-toggle-point-features + * @implements module:web-toggle-point-features.FeaturesStore + * @implements module:web-toggle-point-features.ContextFeaturesStore */ return { providerFactory: () => { diff --git a/packages/features/src/ssrBackedReactContext/store.js b/packages/features/src/ssrBackedReactContext/store.js index ea91d06..44b624f 100644 --- a/packages/features/src/ssrBackedReactContext/store.js +++ b/packages/features/src/ssrBackedReactContext/store.js @@ -3,9 +3,9 @@ import reactContextFeaturesStoreFactory from "../reactContext/store"; /** * A factory function used to create a store for features, held in a {@link https://reactjs.org/docs/context.html|React context}, backed by server-side rendering. - * A wrapper around a {@link module:toggle-point-features.reactContextFeaturesStore|reactContextFeaturesStore}, with server-side rendering supplied by the {@link module:toggle-point-ssr|toggle-point-ssr} package. - * @memberof module:toggle-point-features - * @returns {module:toggle-point-features.ssrBackedReactContextFeaturesStore} A store for features, held within a {@link https://reactjs.org/docs/context.html|React context}. + * A wrapper around a {@link module:web-toggle-point-features.reactContextFeaturesStore|reactContextFeaturesStore}, with server-side rendering supplied by the {@link module:web-toggle-point-ssr|toggle-point-ssr} package. + * @memberof module:web-toggle-point-features + * @returns {module:web-toggle-point-features.ssrBackedReactContextFeaturesStore} A store for features, held within a {@link https://reactjs.org/docs/context.html|React context}. */ const ssrBackedReactContextFeaturesStoreFactory = ({ namespace = "toggles", @@ -18,9 +18,9 @@ const ssrBackedReactContextFeaturesStoreFactory = ({ /** * @name ssrBackedReactContextFeaturesStore - * @memberof module:toggle-point-features - * @implements module:toggle-point-features.FeaturesStore - * @implements module:toggle-point-features.ContextFeaturesStore + * @memberof module:web-toggle-point-features + * @implements module:web-toggle-point-features.FeaturesStore + * @implements module:web-toggle-point-features.ContextFeaturesStore */ return { providerFactory: () => { diff --git a/packages/react-pointcuts/docs/CHANGELOG.md b/packages/react-pointcuts/docs/CHANGELOG.md index 932854d..cd6be4f 100644 --- a/packages/react-pointcuts/docs/CHANGELOG.md +++ b/packages/react-pointcuts/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.2] - 2024-12-26 + +### Fixed + +- updated some errant JSDoc namespaces + ## [0.4.1] - 2024-12-17 ### Removed @@ -33,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - a `Map` of features (de-coupling from a webpack-specific data structure) - a [javascript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), rather than its `default` export (preparing for support of named exports) - updated to `react@18.3.1`, set minimum required react to `17` - - technically a breaking change, but `jsx-runtime` already introduced in [version 1.0.0](#100---2023-09-05)... so was already broken, oops. + - technically a breaking change, but `jsx-runtime` already introduced in [version 0.1.0](#010---2023-09-05)... so was already broken, oops. - moved package to `"type": "module"` and renamed commonJs exports to have `.cjs` extension to prevent `[ERR_REQUIRE_ESM]` errors in consumers that aren't `"type": "module"` ### Fixed diff --git a/packages/react-pointcuts/docs/README.md b/packages/react-pointcuts/docs/README.md index 8fb4694..92347dc 100644 --- a/packages/react-pointcuts/docs/README.md +++ b/packages/react-pointcuts/docs/README.md @@ -18,7 +18,7 @@ Both accept plugins, currently supporting a hook called during code activation ( ## Usage -See: [JSDoc output](https://asos.github.io/web-toggle-point/module-asos-web-toggle-point-react-pointcuts.html) +See: [JSDoc output](https://asos.github.io/web-toggle-point/module-web-toggle-point-react-pointcuts.html) > [!WARNING] > ### Use with React 17 diff --git a/packages/react-pointcuts/package.json b/packages/react-pointcuts/package.json index 62282e7..7ade338 100644 --- a/packages/react-pointcuts/package.json +++ b/packages/react-pointcuts/package.json @@ -1,7 +1,7 @@ { "name": "@asos/web-toggle-point-react-pointcuts", "description": "react pointcut code", - "version": "0.4.0", + "version": "0.4.2", "license": "MIT", "type": "module", "main": "./lib/main.es5.cjs", diff --git a/packages/react-pointcuts/src/external.js b/packages/react-pointcuts/src/external.js index ef62d00..d3834a4 100644 --- a/packages/react-pointcuts/src/external.js +++ b/packages/react-pointcuts/src/external.js @@ -2,17 +2,6 @@ * Code expected in the host application * @external HostApplication */ -/** - * A factory for SDKs; should return an instance of asos-web-features when called with "features" - * @callback external:HostApplication.sdkInstanceProvider - * @async - * @type {Function} - * @param {string} sdkName Name of the SDK to access; will be passed "features" - * @returns {external:asos-web-features} - * @see SiteChrome SDK interface {@link https://asoscom.atlassian.net/wiki/spaces/WEB/pages/593592455/SCP+-+Interface+Definition#SDK-Instances|SDK Instances} - * @example - * const sdkInstance = await sdkInstanceProvider("features"); - */ /** * A delegate passed to log an error * @callback external:HostApplication.logError diff --git a/packages/react-pointcuts/src/getCodeSelectionPlugins.js b/packages/react-pointcuts/src/getCodeSelectionPlugins.js index 7dde7b2..4aa9b83 100644 --- a/packages/react-pointcuts/src/getCodeSelectionPlugins.js +++ b/packages/react-pointcuts/src/getCodeSelectionPlugins.js @@ -1,6 +1,6 @@ /** * A plugin for the point cuts package - * @typedef {object} module:toggle-point-react-pointcuts~plugin + * @typedef {object} module:web-toggle-point-react-pointcuts~plugin * @property {string} name plugin name, used as a prefix when creating {@link https://legacy.reactjs.org/docs/higher-order-components.html|React Higher-Order-Components} when toggling * @property {Function} onCodeSelected hook to be called when a code selection is made */ diff --git a/packages/react-pointcuts/src/index.js b/packages/react-pointcuts/src/index.js index fc91576..b31c5df 100644 --- a/packages/react-pointcuts/src/index.js +++ b/packages/react-pointcuts/src/index.js @@ -4,6 +4,6 @@ import "./external"; /** * Application code for creating a React {@link https://en.wikipedia.org/wiki/Pointcut|pointcut}. - * @module toggle-point-react-pointcuts + * @module web-toggle-point-react-pointcuts */ export { withTogglePointFactory, withToggledHookFactory }; diff --git a/packages/react-pointcuts/src/withTogglePointFactory/index.js b/packages/react-pointcuts/src/withTogglePointFactory/index.js index cd8e6f0..ecd92a2 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/index.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/index.js @@ -7,17 +7,17 @@ import getCodeSelectionPlugins from "../getCodeSelectionPlugins"; {} /** * A factory function used to create a withTogglePoint React Higher-Order-Component. - * @memberof module:toggle-point-react-pointcuts + * @memberof module:web-toggle-point-react-pointcuts * @inner * @function * @param {object} params parameters * @param {function} params.getActiveFeatures a method to get active features. Called honouring the rules of hooks. * @param {external:HostApplication.logError} params.logError a method that logs errors * @param {string} [params.variantKey=bucket] A key use to identify a variant from the features data structure. Remaining members of the feature will be passed to the variant as props. - * @param {Array} [params.plugins] plugins to be used when toggling + * @param {Array} [params.plugins] plugins to be used when toggling * Will be used when a toggled component throws an error that can be caught by an {@link https://reactjs.org/docs/error-boundaries.html|ErrorBoundary}. * When errors are caught, the control/base code will be used as the fallback component. - * @returns {module:toggle-point-react-pointcuts.withTogglePoint} withTogglePoint React Higher-Order-Component. + * @returns {module:web-toggle-point-react-pointcuts.withTogglePoint} withTogglePoint React Higher-Order-Component. * @example * const withTogglePoint = withTogglePointFactory({ * getActiveFeatures, @@ -37,7 +37,7 @@ const withTogglePointFactory = ({ /** * A React Higher-Order-Component that wraps a base / control component and swaps in a variant when deemed appropriate by a context * @function withTogglePoint - * @memberof module:toggle-point-react-pointcuts + * @memberof module:web-toggle-point-react-pointcuts * @param {ReactComponentModuleNamespaceObject} controlModule The control / base module * @param {external:React.Component} controlModule.default The control react component * @param {Map} featuresMap A map of features and their variants, with features as top-level keys and variants as nested keys with modules as the values. diff --git a/packages/react-pointcuts/src/withToggledHookFactory/index.js b/packages/react-pointcuts/src/withToggledHookFactory/index.js index 61baeb7..13c7413 100644 --- a/packages/react-pointcuts/src/withToggledHookFactory/index.js +++ b/packages/react-pointcuts/src/withToggledHookFactory/index.js @@ -6,13 +6,13 @@ import getCodeSelectionPlugins from "../getCodeSelectionPlugins"; {} /** * A factory function used to create a withToggledHook React hook, wrapping an existing hook/function. - * @memberof module:toggle-point-react-pointcuts + * @memberof module:web-toggle-point-react-pointcuts * @inner * @function * @param {object} params parameters * @param {function} params.getActiveFeatures a method to get active features, which is called honouring the rules of hooks. - * @param {Array} [params.plugins] plugins to be used when toggling - * @returns {module:toggle-point-react-pointcuts.withToggledHook} withToggledHook hook function, use to wrap a function (either a hook itself, or a function that must be called wherever a hook can...). + * @param {Array} [params.plugins] plugins to be used when toggling + * @returns {module:web-toggle-point-react-pointcuts.withToggledHook} withToggledHook hook function, use to wrap a function (either a hook itself, or a function that must be called wherever a hook can...). * @example * const getActiveFeatures = () => useContext(myContext); * const withToggledHook = withToggledHookFactory({ @@ -28,7 +28,7 @@ const withToggledHookFactory = ({ getActiveFeatures, plugins = [] }) => { /** * A React hook that wraps a base / control function or hook and swaps in a variant when deemed appropriate by a context * @function withToggledHook - * @memberof module:toggle-point-react-pointcuts + * @memberof module:web-toggle-point-react-pointcuts * @param {ReactHookModuleNamespaceObject} controlModule The control / base module * @param {(external:React.Hook|function)} controlModule.default The control react hook or function. * @param {Map} featuresMap A map of features and their variants, with features as top-level keys and variants as nested keys with modules as the values. diff --git a/packages/ssr/docs/CHANGELOG.md b/packages/ssr/docs/CHANGELOG.md index 75ca4ea..31ed972 100644 --- a/packages/ssr/docs/CHANGELOG.md +++ b/packages/ssr/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.2.1] - 2024-12-26 + +### Fixed + +- updated some errant JSDoc namespaces + ## [0.2.0] - 2024-12-17 ### Removed @@ -50,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - updated to latest `@testing-library/react` to remove errant warning about import of `act` - updated to `react@18.3.1`, set minimum required react to `17` - - technically a breaking change, but `jsx-runtime` already introduced in [version 1.0.0](#100---2023-09-05)... so was already broken, oops. + - technically a breaking change, but `jsx-runtime` already introduced in [version 0.1.0](#010---2023-09-05)... so was already broken, oops. - renamed commonJs exports to have `.cjs` extension to prevent `[ERR_REQUIRE_ESM]` errors in consumers that aren't `"type": "module"` ## [0.1.2] - 2024-12-06 diff --git a/packages/ssr/docs/README.md b/packages/ssr/docs/README.md index 78845c2..54873d8 100644 --- a/packages/ssr/docs/README.md +++ b/packages/ssr/docs/README.md @@ -28,7 +28,7 @@ For the browser: ## Usage -See: [JSDoc output](https://asos.github.io/web-toggle-point/module-asos-web-toggle-point-ssr.html) +See: [JSDoc output](https://asos.github.io/web-toggle-point/module-web-toggle-point-ssr.html) > [!WARNING] > ### Use with React 17 diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 2a6578d..10b2ff7 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,7 +1,7 @@ { "name": "@asos/web-toggle-point-ssr", "description": "toggle point server side rendering code", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "type": "module", "main": "./lib/main.es5.cjs", diff --git a/packages/ssr/src/external.js b/packages/ssr/src/external.js index 3603746..68c0153 100644 --- a/packages/ssr/src/external.js +++ b/packages/ssr/src/external.js @@ -2,17 +2,6 @@ * Code expected in the host application * @external HostApplication */ -/** - * A factory for SDKs; should return an instance of asos-web-features when called with "features" - * @callback external:HostApplication.sdkInstanceProvider - * @async - * @type {Function} - * @param {string} sdkName Name of the SDK to access; will be passed "features" - * @returns {external:asos-web-features} - * @see SiteChrome SDK interface {@link https://asoscom.atlassian.net/wiki/spaces/WEB/pages/593592455/SCP+-+Interface+Definition#SDK-Instances|SDK Instances} - * @example - * const sdkInstance = await sdkInstanceProvider("features"); - */ /** * A delegate passed to log a warning * @callback external:HostApplication.logWarning diff --git a/packages/ssr/src/index.js b/packages/ssr/src/index.js index cbba6c8..e4d6750 100644 --- a/packages/ssr/src/index.js +++ b/packages/ssr/src/index.js @@ -3,7 +3,7 @@ import withJsonIsomorphism from "./withJsonIsomorphism"; import "./external"; /** - * Server Side Rendering code for Isomorphic React applications - * @module asos-web-toggle-point-ssr + * Server Side Rendering code for isomorphic / universal applications + * @module web-toggle-point-ssr */ export { withJsonIsomorphism, serializationFactory }; diff --git a/packages/ssr/src/serializationFactory/index.js b/packages/ssr/src/serializationFactory/index.js index 53ab300..c3e6794 100644 --- a/packages/ssr/src/serializationFactory/index.js +++ b/packages/ssr/src/serializationFactory/index.js @@ -5,16 +5,16 @@ import parse from "html-react-parser"; {} /** * A factory for creating a serialization object that has methods for serializing and deserializing JSON data in server-rendered web applications. - * @memberof module:asos-web-toggle-point-ssr + * @memberof module:web-toggle-point-ssr * @inner * @function - * @memberof module:asos-web-toggle-point-ssr + * @memberof module:web-toggle-point-ssr * @inner * @function * @param {object} params parameters * @param {string} params.id The id attribute of the backing application/json script. * @param {external:HostApplication.logWarning} params.logWarning A method that logs warnings; will be used when malformed JSON is found in the backing store when deserialize on the client, which should only be possible if processed in a system downstream from the origin. - * @returns {module:asos-web-toggle-point-ssr.serialization} Some serialization / deserialization methods + * @returns {module:web-toggle-point-ssr.serialization} Some serialization / deserialization methods * @example * const logWarning = (warning) => console.log(warning); * const id = "app_features"; @@ -23,33 +23,33 @@ import parse from "html-react-parser"; const serializationFactory = ({ id, logWarning }) => /** * @typedef {function} getScriptMarkup - * @memberof module:asos-web-toggle-point-ssr + * @memberof module:web-toggle-point-ssr * @param {object} params parameters * @param {object} params.content The JSON content to be serialized. */ /** * @typedef {function} getScriptReactElement - * @memberof module:asos-web-toggle-point-ssr + * @memberof module:web-toggle-point-ssr * @param {object} params parameters * @param {object} params.content The JSON content to be serialized. */ /** * @typedef {function} getJSONFromScript - * @memberof module:asos-web-toggle-point-ssr + * @memberof module:web-toggle-point-ssr * @returns {object} The JSON content of the script element. */ /** * An object containing methods for serializing and deserializing JSON data in server-rendered web applications. * @typedef {object} serialization - * @memberof module:asos-web-toggle-point-ssr - * @property {module:asos-web-toggle-point-ssr.getScriptMarkup} getScriptMarkup Gets a string containing markup for a type="application/json" script element with the specified content. - * @property {module:asos-web-toggle-point-ssr.getScriptReactElement} getScriptReactElement - Gets a React element for a type="application/json" script element with the specified content. - * @property {module:asos-web-toggle-point-ssr.getJSONFromScript} getJSONFromScript - Returns the JSON content of the script element. + * @memberof module:web-toggle-point-ssr + * @property {module:web-toggle-point-ssr.getScriptMarkup} getScriptMarkup Gets a string containing markup for a type="application/json" script element with the specified content. + * @property {module:web-toggle-point-ssr.getScriptReactElement} getScriptReactElement - Gets a React element for a type="application/json" script element with the specified content. + * @property {module:web-toggle-point-ssr.getJSONFromScript} getJSONFromScript - Returns the JSON content of the script element. */ ({ /** - * @memberof module:asos-web-toggle-point-ssr.serialization + * @memberof module:web-toggle-point-ssr.serialization * @param {object} content The JSON content to be serialized. * @returns {string} A string containing markup for a type="application/json" script element with the specified content. */ diff --git a/packages/ssr/src/withJsonIsomorphism/index.js b/packages/ssr/src/withJsonIsomorphism/index.js index b68c691..8559f37 100644 --- a/packages/ssr/src/withJsonIsomorphism/index.js +++ b/packages/ssr/src/withJsonIsomorphism/index.js @@ -8,7 +8,7 @@ import { useState, useEffect } from "react"; * which are then realised into a prop when first hydrating on the client. It will be reactive to subsequent non-`undefined` prop values, * for that prop. * The package "browser" export includes the code to read the script, omitted from the "import" / "require" export (server package). - * @memberof module:asos-web-toggle-point-ssr + * @memberof module:web-toggle-point-ssr * @inner * @function * @param {external:React.Component} WrappedComponent The React component that will receive the props. diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index 5102394..9467746 100644 --- a/packages/webpack/docs/CHANGELOG.md +++ b/packages/webpack/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.7.3] - 2024-12-26 + +### Fixed + +- updated some errant JSDoc namespaces + ## [0.7.2] - 2024-12-17 ### Removed diff --git a/packages/webpack/docs/README.md b/packages/webpack/docs/README.md index 823c534..f4c7b1d 100644 --- a/packages/webpack/docs/README.md +++ b/packages/webpack/docs/README.md @@ -20,7 +20,7 @@ The join points are configured using a [glob](https://en.wikipedia.org/wiki/Glob ### Configuration -See [JSDoc output](https://asos.github.io/web-toggle-point/module-asos-web-toggle-point-webpack.html) +See [JSDoc output](https://asos.github.io/web-toggle-point/module-web-toggle-point-webpack.html) Different code paths may have different toggling needs, and may want a toggle point applied in differing ways. Independent point cuts should be configured for each different: diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 38fbc45..ec17f81 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.7.2", + "version": "0.7.3", "license": "MIT", "type": "module", "main": "./lib/main.cjs", diff --git a/packages/webpack/src/index.js b/packages/webpack/src/index.js index a99fcdc..a11c1e9 100644 --- a/packages/webpack/src/index.js +++ b/packages/webpack/src/index.js @@ -2,6 +2,6 @@ import "./external.js"; /** * Webpack code for injecting toggle points - * @module toggle-point-webpack + * @module web-toggle-point-webpack */ export { TogglePointInjection } from "./plugins"; diff --git a/packages/webpack/src/plugins/togglePointInjection/index.js b/packages/webpack/src/plugins/togglePointInjection/index.js index df98970..0148828 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.js @@ -9,7 +9,7 @@ import schema from "./schema.json"; /** * Toggle Point Injection Plugin - * @memberof module:toggle-point-webpack + * @memberof module:web-toggle-point-webpack * @inner */ class TogglePointInjection { From 1f0cb29d36b158f19c1301eba2c122c0810c146b Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:18:52 +0000 Subject: [PATCH 03/89] [26] Fix public/scoped package publishing (#27) * update workflows * version * typo * update chromium linux snaps * versions for serve update * package.json repository field * update root package.lock * bugs & directories/doc fields * fix changelog --------- Co-authored-by: Tom Pereira --- .github/actions/publish/publish.sh | 2 +- .github/workflows/release.yaml | 2 ++ docs/CHANGELOG.md | 6 ++++++ examples/serve/docs/CHANGELOG.md | 7 +++++++ examples/serve/package.json | 2 +- .../-screenshots-control-chromium-linux.png | Bin 1515 -> 1281 bytes ...enshots-st-patricks-day-chromium-linux.png | Bin 3354 -> 3079 bytes package-lock.json | 14 +++++++------- package.json | 2 +- packages/features/docs/CHANGELOG.md | 6 ++++++ packages/features/package.json | 13 ++++++++++++- packages/react-pointcuts/docs/CHANGELOG.md | 6 ++++++ packages/react-pointcuts/package.json | 13 ++++++++++++- packages/ssr/docs/CHANGELOG.md | 6 ++++++ packages/ssr/package.json | 13 ++++++++++++- packages/webpack/docs/CHANGELOG.md | 6 ++++++ packages/webpack/package.json | 13 ++++++++++++- 17 files changed, 97 insertions(+), 14 deletions(-) diff --git a/.github/actions/publish/publish.sh b/.github/actions/publish/publish.sh index cd06cdf..cd8920a 100755 --- a/.github/actions/publish/publish.sh +++ b/.github/actions/publish/publish.sh @@ -1,2 +1,2 @@ TAG=$([ "$PRE_RELEASE" == "true" ] && echo "--tag=pre-release ") -npm publish $TAG--workspace=$WORKSPACE 2> publish_stderr_digest.log +npm publish --access public $TAG--workspace=$WORKSPACE 2> publish_stderr_digest.log diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index eb97d40..ef7b596 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -219,3 +219,5 @@ jobs: name: ${{ matrix.package.name }} version: ${{ steps.newVersion.outputs.version }} is-pre-release: ${{ env.IS_PRE_RELEASE }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 532c7d8..42e9399 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,12 @@ N.B. See changelogs for individual packages, where most change will occur: This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). +## [0.10.3] - 2025-02-27 + +### Fixed + +- GHA pipelines for publishing to public/scoped NPM repository + ## [0.10.2] - 2024-12-26 ### Fixed diff --git a/examples/serve/docs/CHANGELOG.md b/examples/serve/docs/CHANGELOG.md index 126a3c8..9f2b321 100644 --- a/examples/serve/docs/CHANGELOG.md +++ b/examples/serve/docs/CHANGELOG.md @@ -5,6 +5,13 @@ 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.2.3] - 2025-02-27 + +### Changed + +- updated some linux playwright snapshots + - no code changes, so this must be a change in linux chromium. Assets look identical to eye, so presumably need to relax the fuzziness. + ## [0.2.2] - 2024-12-17 ### Removed diff --git a/examples/serve/package.json b/examples/serve/package.json index 945644b..20a3f5d 100644 --- a/examples/serve/package.json +++ b/examples/serve/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-serve-example", - "version": "0.2.0", + "version": "0.2.3", "type": "module", "private": true, "scripts": { diff --git a/examples/serve/src/fixtures/event/playwright.spec.ts-snapshots/-screenshots-control-chromium-linux.png b/examples/serve/src/fixtures/event/playwright.spec.ts-snapshots/-screenshots-control-chromium-linux.png index 6615aa1a81dce531b97b8f2d111a015ab6a08e19..f1aa0ebed6c0d579a89a296353976c95a38abd46 100644 GIT binary patch delta 1249 zcmV<71RneA3xNudFn)my&Yb+>UG`%YU6`?^f*U%c*q61R@!uK()RUrEOE zp4{I7pq}kA#OdlH001CSx2CS*gd+OMdUO6JcDvo8#RP%DFF zbljMgS+zGGRDX&b%W_hAuu78Epe%8P0w+~Qo8*RjZM2FWr8wAZIz_%RPfjND3a|Ep zy8&lqR_!fjjgr6?3Y<`C&i1ESHigqcg~swc>zA#25mHW(O@Xs@$!N|Nog!D6-x6oi zFeIKWu$*Kx=fTaAuKMzf6d9BRyWMUVj10cOtCh}Vv44q@vywk12G5r|^G%c_AInii z;-uy>IYwuBwJ{H(D45kGRwxI98%o2Ih&efQQObVUE^7qWsS+H z&}d??>fmryE^55g`8ujsTlXWRCjF)Yw}dTR4}ZFY{yb)wXMf`jQD5at2ZMSO^ zPnnZHjbqUdfsT(0DK;iS`qZVm4!o(0NgIQ3c+VIEAZqfdP>3L;^y-y39Gh~>0{{Sk zEq}*r$(P%=BiyeLe_MfnZV4-1y+z%6>sV=NpU!|`4ke}c<)IYmhkEG#{^dEuT7B#Q z4fn@uA_8zN{rM3-b25j=7YnVs9-GXC4}?$Jm<080k7{7*V$wzcgm;Y{02t;x?sX{u z07S;VPU+1mKuUzPq~B72+IFh2k6<44<$s|R*_3<(vD+o}>GpgPBE(Qik*uJ#?Vf#Q z<2&|NRK!Pz+Jh7Yt=%<)J^^ufSw)%+HVJa3u4lX0G|`g*Q(++7yFvK0QLd1NbR#p_G1RU3>dv2NG=FOXT?%bg|^jDJRdhu{3 z003sQxvsA6>VMU%008iKy!Q6?l9G}$XU=4F!Zd#oD2nR!`ttJf-Q8U@hRw~*_V#v> zNW|fAsNF;K*-2`3Y`gpzG!-o%VZ*OBh4Gj%Xo;)E*2!dcqNr}Z` zSzTQ<8jUO#%j5B&D7v$=)6~?|)zuY=LK9u2d?0^X84y=@bfuo12?>_@hUU8XFs#p21yNs@#znGCn*=jX4luM=Lxfqy{Y)TvWgyQQV&?%lfthqxKYSCN_mafN;G z-~nMicDwz;g$o=G2e)AP6YfhwDaaRM#sD91ai!1^=?Wy`8+zi4!LZ3JNS1%f!TlR;x`iAqiPoS$JSh zPEH0_K{Yn6uyMoZ=H@mwHe&7R?0-uJl*FELrKP3A!^43<06~zKFJDeiPv5w4Ln4t> zRaITPcFpVcuC1+&jEp>f{J5*DE6LQw#l@E{UD9YYU%!4027|M+vllO3G#CuY`{;DK zTeof%78VKwf+Q1WoOFur6&ow;z`#Ikcv)H5*|TSBYiqxK`xXj?j7DQoQGZb=6iRbn zGN2@O$9eVYl}sk%a=BbCx1yrLVzFTMt*xz&jt;R{oSmI5lgU1Q{D@zXKoI=#;|JC@ z8jTed75I=+sl+Z*csQHQmPjP{2hnIqe0)A1004u7g9)9ZCxfDBV`JmPhYtzYw^#@l z;k_~>QY16DxJI9zHa0fe+S)`S5u42}FE9V}=@X@WA(0{-B^mTT_h_g> n|6oZ&8T!!BP=-DuSX(O1u+tD@ zm(@Y&RzWYTjGbEFVzVc&FX_s;vjXnY_%H#E5Z-uU{D#rR%+{;Xea-wCU_j%VA` zwo`Gv%X6t;alM<3=C={vL^@H>)T4UdyYv0CdqO3DL&)EX`nYrFqq&{9MPawq+AY-B zESuig_dv9#cc5>mI)8rYmc3Um@8$a2apg_sQl??PJAV@Zd~{IY*iPAJ+9U zu7}85^pU~G-m6aB!}rHHetxlb_L6_8wlF}ph#Nn*`rTMDReWZ&24HaCUbDDIreNNr zq5tbMuYU(ypWKrW+6noi1m=vV(-R&*5?V2}^5flLLj=l$r{Y~PsKi78yfCcY=3v?( zs>@EgDeQI%>A3TKj-Laf;UXJ9Ti7Wa*hoIUz9+m`Ti|=5+xj>EUx0l(zEhd|WhBxw zK!*Bz<4t|}u3Kw-lJ(s(1pp!pXG;5R`GlMn?|+sd&z~+99G8VeR?=F`vptUxN&rel z+WziLrDQlRs;)CNHiaEzJncYGl}sYOs*OrHUZi&i9Wg-w7^N%y#~debjGIoC5vu4) zIw^Kk0qqcV8=Q1g*zFWjPb3o77I{PgZ0_xU|AU`=dh1{QyYpA!;z9(tcf+T)fBe%w z{C~yQYxMwi#Q1xkRQ{u>FCmfKl6(5xEHIYNCmc1Kk!1?dkxYRIQp+&>>0*AoWXIFR zJ+e1lwi%Iz6p;X-S9VI4$An>B7q2u!zCDr8+otU^5J4G9XB4$bl2@`E(!>+eW7GPZ z8UU0{<TM5?1xRkg@Cd z=7g1PI0$h80P|gsh|5c=Neo@&Cv+{lSe&$3Oid~Ri2~e>h2)#Bs`^_@-D?=b)IO0) z9V?m#%EggZwRDq2b(`(w=%%pS*|mGc{_gQ#K6n4%1_0aoH}^z(rrCTye@|uZSAU@5}Ai?bkBpm?BoKu~~e6uVwC$N$3<_)KXJS z-DegRnfRsDQQZ;>)vZlN2%~9ahTtLQ#0h)#cL4#tDq;%D;`$E&Jd( zf~3o=ls|Jii0(M7myIDaapVt2H$VG7Pi2~p6s)-1rkpl3cXwWLGRC>700W=>l#$<` zlu4_A#V+yVPaDZ%W^|73+9DC(KFP>n%~uZirIF>;>?Ea^kC9#f=_FJFfPWRVBZtQN zpZHO#n20q?B6Hg(s%en7%=YaO_A0w}i}l5~&W>*S*b<4+NKd`KcqyEHXVzIe!J{MLwFds*U^<^rMHq zH-*IGwv#UBV&a{ivFw@Gq za>@JFdq00P;h#FB7IJp$N9zhdtdx53>Cri|@5{FRHLLth=S^j0LE01M40u_Tmy}Tz z5__M2Ue%cAvZhHSZ4=cZ$oAR(P=&opYv*_;7JIx6eSG8V`QU>G)^FO_H+11QTy$L! z>G3wK;}_~Rf8i2<>VL%rg!gT~=QFK+va0J;96y#Y^c1S4Wd5;Z=bq6DzLm-ePQj{y z- zW}e7eAOOJ0k8YzhWWcgvwUuRDk)wv~xt3RQdCichYc@9~lz#%?wbbs$oF{6-Qf9^x z1qq?e0K^)L41&;SA)$oQ7^NIo$oxJ#bxtxT{LmM+r~cb}j*HYU1dZ7Q>j`)M-FI7fV}3DJNFJLF5PkGt z(j@?hB{Irqi^op}?-ufIE>raZh=SZmBO#Omv$L)j0vux@2w9?t5DM^=(rYVYKu}}= z!^+@Wr>U4LB~%IkZrO2(Aj>z+Ks$;xod0yFa}V1WwAjB2Y}KTX&G=g_*ptDqBM5(<^+wBSvCgA!wP?T z%$;^1D*zDM`I1qlN#hZfwMZh`B&ua<+GV>Hx-0C}Tf3cGzPOQmqF%29k-^v}AKv!u zFMZ;>y?@dET77|V8y?;H&oc{)?=4(v7)PT$y?e3qvwR%Oti< zltbxxE=yFX*r4{aY74C)MJ&NYWF)jCm=!KVC^Uxzh*?(sRkSar56r*swQvNl!H0y|*B;80kU2Ze~ zB?N)%dw>K0%&~p&Lz+rj#^85@pLjwa%$}UFO~=JYX3T1wf?cNxTW7l&2m`KK|fsV7npUE(`nS-?eMNIR4d#9{BRZ+kd|T zU~ay0YUY*d{Dp0s)FJv%G_p!@dXq-_HKQ~$6)gS04-6=}nm1&L00`aUtI704*{^{P zDBA99ZjUMe%(u%Gk8zSJu5iHOd6r%AJm!OH`f#^SU>vh-`9ebP?094e%o$JWmgRGz zB4u;hiYJ@PY@Ui2zB%sEp>$z?LVo~r%6j62&EjeEV7i$s&^>EQSn-te`mTHp9~PKzl_p(KMH`kV;5r4HCK9XfgstT2qn5;ug>KzpA)UD} zp8u~W4}bB+jq8Peeq#q7*06@V5FHCUn$2cAxZkG!``%}j@4xX4u7{U|-|xV~8rE#=KC!$$(03H$#6xPes0BF2(|00000NkvXXu0mjf DQGgC^ delta 3339 zcmV+m4fOJd7@8W8Fn-0yzBd+#^j+yT++_0Wet^r2@!&AUxQD1R;ONgw*~_F;Bn59&Zj}_CYeRQ3cNTqk7eVNWCV7)R%N_ybZTr&e=wV`aduvzM`jGs=py96l zm`-*~YZi@4Q-42e{?l|LBAKYDuBh-uVM$|2i>ie@S<+ZS=!x$7^UuSv;$ssJO$-%; z-mSa)c0Ium?5Dz?@HCwL9XtJI`rCD;x6#AIHfd|8(J94>DlWqlL#Z}&7pYoAo~P6P zx}AozO)+(?>e|vXOUV>rQCKW0OC%H7&};x8%p)vkPJfOmM*i8D;5A{^u~|NLJ|i4Q zylr1#{DARkm(zUv`6dS?zinSE4+|!C*M)So{MT#3?~+jSbz!lM~vdDX~%g{K#09-G;$Y+ezv!W57H zUikUe&wnQ$pCoj|mgp_R9EO>~v7%bBs4O2lpEvI8Yv5+>1^`nAP3i4{OcBbmMl6`v zCKXcNV*yT+0bp94@>caeKc|nlbbBUBC{p@Qg6U<2QhUnty_`PtK|c)x44OoUNSi)ZVdY+G}(g zFRm92qnT)=xS{yudmlX}L1eW7XNm zYk#yl4Fcd~C@Am(9#J}VH;xPpK z(C0%2&M@xAjpsKG+&kcE?YcT_b>WM`dTD*QN4Pc3T1}`kOERCld@_E(_z>3+13$Yo zTTmpZmQ-^Y-06d-FY;f6BCmfT?8pps34h#_zv)`lwGB}l0ASVmRX3mC%$$|!Yw!D7 zvt>{xncX@qA)K}|t|UqXYtA-go2firUAbALkJLKzzv zgY5=m+kaK3xYn;K8$(dNUN6^(Yg-=cbozp7fmS8tQJJc6Z%GULeXp|k5{_3@eUF$ZJF-d(k%I!qAud)eupmC&V0#iQFF<{7dk#{|w`&^XsH_ zZ8_wsvF498GltBNuYZvXrV94o-5;G1ttQk)e6WZ)@ z`XXRYz`2TZQTwB=&%b`6J|zB(}zsYxSz2$Z*78K0syE8)#m}9FZN#y z0OJOX%PGrQ61ZeZ;1a_op6vcrw^Rdwhpor=e}0dnID#M;g2j5rDm03Ao)}?jlKRVE!5$zB?x_^a$>*wQbul2 zM>~%OfPZLy^v3*+QT?NKP2L3nYC@e`o;$*EM1R}%ukXg0fi8iWC7HKg+=}y!qheGe zWHq4%0B@c*9e=fek?$K({iDb=l1*i6by@^LevJ9Cqb#M3rENJFf+@91!_*9uwWZq{ zcev;JWR+%-d_#AEe0L=$*cQRA!Ga>e1JMKWP=BdaF8qCAf7|{k5h+H3NXlZ0P!>@e zL$JH7=XcZ0e1_Pl<(L~4T zL9RgzoMFV_p6lal@7pe9Ib}Iy`=QipUhDK9)Q#k-!czWTBkT0khb=Txw34RHAm3b1C zBw<*>?4h$ASPrlC#|SfqACqz^#ofj|%p=S=naAX1&B`)#_SJ+s#4RMmEkr054tE$% z#i+Ml+**HSeWkdP!{Bs`tEa7JTYruYbaIXSnfMubRiRasH52(wxeK2c4;<>pk(g01&hKu|H=8qxr+n$1lrJSm=xyGxTBK>P9nc& zbQZ*J3AH79%iQ5}Ev|MiC3d&}*N(*2KN9>sCXe-+0{}90E~KXpMsi#aRJv28|94>mBh`)t?kv$g4lDRxW7SfTz zb!tMH7`)kxdr2s?irUs%1VQYX_J(bq0a~3_5Bk?et`EHpp;&mj{C_l`%a7(qBYyk$FIYD25Qpi=qH)Zy(1$*}gD^X>v6Pe)Gsfex zoF&ReL4Goh(tI4pm|>w0eRv09c4C|PL;?UUp?=a(=*b&F@8o^14}JJM!o0-(AFhOd V$k~JKp#T5?07*qoL Date: Thu, 6 Mar 2025 14:23:30 +0000 Subject: [PATCH 04/89] fix registry url etc (#34) Co-authored-by: Tom Pereira --- .github/actions/publish/action.yaml | 2 ++ docs/CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/actions/publish/action.yaml b/.github/actions/publish/action.yaml index bb3659c..2106e92 100644 --- a/.github/actions/publish/action.yaml +++ b/.github/actions/publish/action.yaml @@ -21,6 +21,8 @@ runs: - uses: actions/setup-node@v4 with: node-version-file: .nvmrc + registry-url: https://registry.npmjs.org/ + always-auth: true cache: npm - name: Install diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 42e9399..bfcd51b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,12 @@ N.B. See changelogs for individual packages, where most change will occur: This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). +## [0.10.4] - 2025-03-06 + +### Fixed + +- Ensured that the registry is explicitly set, to ensure that [`@actions/setup-node`](https://github.com/actions/setup-node) honours the `NODE_AUTH_TOKEN` + ## [0.10.3] - 2025-02-27 ### Fixed diff --git a/package.json b/package.json index a2992c6..540f361 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@asos/web-toggle-point", - "version": "0.10.3", + "version": "0.10.4", "repository": "git@github.com:asos/web-toggle-point.git", "homepage": "https://asos.github.io/web-toggle-point/", "license": "MIT", From 9c2e7b356a171574f477e9f9fecce83fd6666d49 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:26:46 +0000 Subject: [PATCH 05/89] update pathSegmentToggleHandler to accept a Map --- .../pathSegmentToggleHandler.js | 4 ++-- .../pathSegmentToggleHandler.test.js | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js index 99a2633..5aac6a0 100644 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js +++ b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js @@ -3,7 +3,7 @@ const buildTree = (map = new Map(), parts, variants, key) => { if (rest.length) { map.set(part, buildTree(map.get(part), rest, variants, key)); } else { - map.set(part, variants(key)); + map.set(part, variants.get(key)); } return map; }; @@ -15,7 +15,7 @@ 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} options.variants a Map of posix relative file paths to variant modules * @returns {function} A handler of join points injected by the plugin */ const pathSegmentToggleHandler = ({ togglePoint, joinPoint, variants }) => { diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js index 6e64730..d13217f 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, variants; beforeEach(() => { jest.clearAllMocks(); @@ -16,15 +14,19 @@ describe("pathSegmentToggleHandler", () => { [1, 2, 3].forEach((segmentCount) => { const keyArray = [...Array(segmentCount).keys()]; - describe(`given a list of variant paths with ${segmentCount} path segments (after the variants path)`, () => { + describe(`given a Map keyed by variant paths with ${segmentCount} path segments (after the variants path)`, () => { 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`]: + variants = 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 }); }); @@ -44,14 +46,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(variants)) { 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(variants.get(key)); } }); }); From a46f3d1ab20bfa51c7a8dcd750f89e8cdb1887b8 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:29:11 +0000 Subject: [PATCH 06/89] update join point generation, and add options to schema --- .../plugins/togglePointInjection/schema.json | 21 ++- .../setupSchemeModules/generateJoinPoint.js | 47 ++++-- .../generateJoinPoint.test.js | 151 +++++++++++++++--- 3 files changed, 179 insertions(+), 40 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/schema.json b/packages/webpack/src/plugins/togglePointInjection/schema.json index 21094df..17eda73 100644 --- a/packages/webpack/src/plugins/togglePointInjection/schema.json +++ b/packages/webpack/src/plugins/togglePointInjection/schema.json @@ -6,14 +6,27 @@ "items": { "type": "object", "properties": { + "joinPointResolver": { + "instanceof": "Function" + }, + "loadingMode": { + "anyOf": ["dynamicImport", "dynamicRequire", "static"] + }, "name": { "type": "string" }, - "togglePointModule": { "type": "string" }, - "variantGlob": { "type": "string" }, "toggleHandler": { "type": "string" }, - "joinPointResolver": { - "instanceof": "Function" + "togglePointModule": { "type": "string" }, + "variantGlob": { "type": "string" } + }, + "if": { + "properties": { + "loadingMode": { "const": "dynamicImport" } + } + }, + "then": { + "properties": { + "webpackMagicComment": { "type": "string" } } }, "additionalProperties": false, diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js index cab2b71..28a0460 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js @@ -1,19 +1,46 @@ -import { posix } from "path"; -import regexgen from "regexgen"; import { POINT_CUTS, SCHEME } from "../constants.js"; -const { dirname } = posix; + +const getStatic = ({ path, variants }) => { + const variantsKeys = Array.from(variants.keys()); + const code = `import * as joinPoint from "${path}"; +${variantsKeys + .map( + (key, index) => `import * as variant_${index} from "${variants.get(key)}";` + ) + .join("\n")} +const variants = new Map([ +${variantsKeys.map((relativePath, index) => ` ["${relativePath}", variant_${index}]`).join(",\n")} +]);`; + + return code; +}; + +const getDynamic = (method, webpackMagicComment, { path, variants }) => { + const variantsKeys = Array.from(variants.keys()); + const code = `const joinPoint = () => ${method}(${webpackMagicComment}"${path}"); +const variants = new Map([ +${variantsKeys.map((key) => ` ["${key}", () => ${method}(${webpackMagicComment}"${variants.get(key)}")]`).join(",\n")} +]);`; + + return code; +}; + +const getDynamicRequire = getDynamic.bind(undefined, "require"); +const getDynamicImport = getDynamic.bind(undefined, "import"); const generateJoinPoint = ({ joinPointFiles, path }) => { const { - pointCut: { name }, + pointCut: { name, loadingMode, webpackMagicComment }, 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} }); + const pointCutImport = `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}";`; + const code = { + dynamicImport: getDynamicImport.bind(undefined, webpackMagicComment), + dynamicRequire: getDynamicRequire.bind(undefined, ""), + static: getStatic + }[loadingMode]({ path, variants, webpackMagicComment }); + return `${pointCutImport} +${code} export default pointCut({ joinPoint, variants });`; }; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js index d03a273..3c32d23 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js @@ -7,41 +7,140 @@ jest.mock("../constants", () => ({ })); describe("generateJoinPoint", () => { - const path = "test-folder/test-path"; + 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" + const relativePaths = [ + "/test-sub-folder/test-variant-1", + "/test-sub-folder/test-variant-2", + "/test-other-sub-folder/test-variant-1" ]; + const variants = new Map( + relativePaths.map((relativePath) => [ + relativePath, + `${path}${relativePath}` + ]) + ); + const pointCut = { name: pointCutName }; let result; - beforeEach(() => { - const joinPointFiles = new Map([ - [path, { pointCut: { name: pointCutName }, variants }] - ]); - result = generateJoinPoint({ joinPointFiles, path }); - }); + const makeCommonAssertions = () => { + 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 appropriate point cut for the join point", () => { - expect(result).toMatch( - `import pointCut from "${SCHEME}:${POINT_CUTS}:/${pointCutName}";` - ); - }); + 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 });" + ); + }); + }; + + describe("with a static loadingMode", () => { + beforeEach(() => { + const joinPointFiles = new Map([ + [path, { pointCut: { ...pointCut, loadingMode: "static" }, variants }] + ]); + result = generateJoinPoint({ + joinPointFiles, + path + }); + }); + + makeCommonAssertions(); + + it("should return a script that statically 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, storing in variables", () => { + expect(result).toMatch(` +import * as variant_0 from "${path}${relativePaths[0]}"; +import * as variant_1 from "${path}${relativePaths[1]}"; +import * as variant_2 from "${path}${relativePaths[2]}";`); + }); - 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 creates a Map of variants, keyed by relative path, valued as the variant module", () => { + expect(result).toMatch(`const variants = 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] +]);`); + }); }); - 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])/ });` - ); + describe("with a dynamicImport loadingMode", () => { + const webpackMagicComment = "/* test loading qualifier */"; + beforeEach(() => { + const joinPointFiles = new Map([ + [ + path, + { + pointCut: { + ...pointCut, + loadingMode: "dynamicImport", + webpackMagicComment + }, + variants + } + ] + ]); + result = generateJoinPoint({ + joinPointFiles, + path + }); + }); + + makeCommonAssertions(); + + it("should return a script that prepares a join point function that will dynamically load the join point, when executed, with any provided webpack loading directives", () => { + expect(result).toMatch( + `const joinPoint = () => import(${webpackMagicComment}"${path}");` + ); + }); + + it("should return a script that creates a Map of variants, keyed by relative path, valued as a function that will dynamically load the variant module when executed, with any provided webpack loading directives", () => { + expect(result).toMatch(`const variants = new Map([ + ["/test-sub-folder/test-variant-1", () => import(${webpackMagicComment}"${path}${relativePaths[0]}")], + ["/test-sub-folder/test-variant-2", () => import(${webpackMagicComment}"${path}${relativePaths[1]}")], + ["/test-other-sub-folder/test-variant-1", () => import(${webpackMagicComment}"${path}${relativePaths[2]}")] +]);`); + }); }); - 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 });"); + describe("with a dynamicRequire loadingMode", () => { + beforeEach(() => { + const joinPointFiles = new Map([ + [ + path, + { + pointCut: { + ...pointCut, + loadingMode: "dynamicRequire" + }, + variants + } + ] + ]); + result = generateJoinPoint({ + joinPointFiles, + path + }); + }); + + makeCommonAssertions(); + + it("should return a script that prepares a join point function that will dynamically load the join point, when executed", () => { + expect(result).toMatch(`const joinPoint = () => require("${path}");`); + }); + + it("should return a script that creates a Map of variants, keyed by relative path, valued as a function that will dynamically load the variant module when executed", () => { + expect(result).toMatch(`const variants = new Map([ + ["/test-sub-folder/test-variant-1", () => require("${path}${relativePaths[0]}")], + ["/test-sub-folder/test-variant-2", () => require("${path}${relativePaths[1]}")], + ["/test-other-sub-folder/test-variant-1", () => require("${path}${relativePaths[2]}")] +]);`); + }); }); }); From 0705eee2d239b3dc1a4996ce10eb2d68173b94ad Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:31:10 +0000 Subject: [PATCH 07/89] generate join point refactor --- .../setupSchemeModules/generateJoinPoint.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js index 28a0460..4dffc8c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js @@ -25,9 +25,6 @@ ${variantsKeys.map((key) => ` ["${key}", () => ${method}(${webpackMagicComment} return code; }; -const getDynamicRequire = getDynamic.bind(undefined, "require"); -const getDynamicImport = getDynamic.bind(undefined, "import"); - const generateJoinPoint = ({ joinPointFiles, path }) => { const { pointCut: { name, loadingMode, webpackMagicComment }, @@ -35,10 +32,10 @@ const generateJoinPoint = ({ joinPointFiles, path }) => { } = joinPointFiles.get(path); const pointCutImport = `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}";`; const code = { - dynamicImport: getDynamicImport.bind(undefined, webpackMagicComment), - dynamicRequire: getDynamicRequire.bind(undefined, ""), + dynamicImport: getDynamic.bind(undefined, "import", webpackMagicComment), + dynamicRequire: getDynamic.bind(undefined, "require", ""), static: getStatic - }[loadingMode]({ path, variants, webpackMagicComment }); + }[loadingMode]({ path, variants }); return `${pointCutImport} ${code} export default pointCut({ joinPoint, variants });`; From 56a2638f0132a04a7fbf9a8bf63a568b86630408 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:34:32 +0000 Subject: [PATCH 08/89] default webpackMagicComment properly --- .../setupSchemeModules/generateJoinPoint.js | 2 +- .../generateJoinPoint.test.js | 69 ++++++++++--------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js index 4dffc8c..169abbc 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js @@ -27,7 +27,7 @@ ${variantsKeys.map((key) => ` ["${key}", () => ${method}(${webpackMagicComment} const generateJoinPoint = ({ joinPointFiles, path }) => { const { - pointCut: { name, loadingMode, webpackMagicComment }, + pointCut: { name, loadingMode, webpackMagicComment = "" }, variants } = joinPointFiles.get(path); const pointCutImport = `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}";`; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js index 3c32d23..f9baf4c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js @@ -70,44 +70,47 @@ import * as variant_2 from "${path}${relativePaths[2]}";`); }); }); - describe("with a dynamicImport loadingMode", () => { - const webpackMagicComment = "/* test loading qualifier */"; - beforeEach(() => { - const joinPointFiles = new Map([ - [ - path, - { - pointCut: { - ...pointCut, - loadingMode: "dynamicImport", - webpackMagicComment - }, - variants - } - ] - ]); - result = generateJoinPoint({ - joinPointFiles, - path + describe.each(["/* test loading qualifier */", undefined])( + "with a dynamicImport loadingMode, and a %s webpackMagicComment", + (webpackMagicComment) => { + const expectedMagicComment = webpackMagicComment ?? ""; + beforeEach(() => { + const joinPointFiles = new Map([ + [ + path, + { + pointCut: { + ...pointCut, + loadingMode: "dynamicImport", + webpackMagicComment + }, + variants + } + ] + ]); + result = generateJoinPoint({ + joinPointFiles, + path + }); }); - }); - makeCommonAssertions(); + makeCommonAssertions(); - it("should return a script that prepares a join point function that will dynamically load the join point, when executed, with any provided webpack loading directives", () => { - expect(result).toMatch( - `const joinPoint = () => import(${webpackMagicComment}"${path}");` - ); - }); + it("should return a script that prepares a join point function that will dynamically load the join point, when executed, with any provided webpack loading directives", () => { + expect(result).toMatch( + `const joinPoint = () => import(${expectedMagicComment}"${path}");` + ); + }); - it("should return a script that creates a Map of variants, keyed by relative path, valued as a function that will dynamically load the variant module when executed, with any provided webpack loading directives", () => { - expect(result).toMatch(`const variants = new Map([ - ["/test-sub-folder/test-variant-1", () => import(${webpackMagicComment}"${path}${relativePaths[0]}")], - ["/test-sub-folder/test-variant-2", () => import(${webpackMagicComment}"${path}${relativePaths[1]}")], - ["/test-other-sub-folder/test-variant-1", () => import(${webpackMagicComment}"${path}${relativePaths[2]}")] + it("should return a script that creates a Map of variants, keyed by relative path, valued as a function that will dynamically load the variant module when executed, with any provided webpack loading directives", () => { + expect(result).toMatch(`const variants = new Map([ + ["/test-sub-folder/test-variant-1", () => import(${expectedMagicComment}"${path}${relativePaths[0]}")], + ["/test-sub-folder/test-variant-2", () => import(${expectedMagicComment}"${path}${relativePaths[1]}")], + ["/test-other-sub-folder/test-variant-1", () => import(${expectedMagicComment}"${path}${relativePaths[2]}")] ]);`); - }); - }); + }); + } + ); describe("with a dynamicRequire loadingMode", () => { beforeEach(() => { From a663a3902750d7713c81fc757ab86a2b123c8ef5 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:38:12 +0000 Subject: [PATCH 09/89] update processVariantFiles --- examples/serve/src/toggleHandlers/singlePathSegment.js | 2 +- .../processPointCuts/processVariantFiles/index.js | 7 ++++--- .../processPointCuts/processVariantFiles/index.test.js | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/serve/src/toggleHandlers/singlePathSegment.js b/examples/serve/src/toggleHandlers/singlePathSegment.js index 9cf4f2d..aa394e5 100644 --- a/examples/serve/src/toggleHandlers/singlePathSegment.js +++ b/examples/serve/src/toggleHandlers/singlePathSegment.js @@ -2,7 +2,7 @@ export default ({ togglePoint, joinPoint, variants }) => { const featuresMap = new Map(); for (const key of variants.keys()) { const [, value] = key.split("/"); - featuresMap.set(value, variants(key)); + featuresMap.set(value, variants.get(key)); } return togglePoint(joinPoint, featuresMap); }; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js index 63629cc..a296591 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js @@ -27,7 +27,7 @@ const processVariantFiles = async ({ } joinPointFiles.set(joinPointPath, { pointCut, - variants: [] + variants: new Map() }); } @@ -39,8 +39,9 @@ const processVariantFiles = async ({ continue; } - joinPointFile.variants.push( - relative(joinDirectory, path).replace(/^([^./])/, "./$1") + joinPointFile.variants.set( + relative(joinDirectory, path).replace(/^([^./])/, "./$1"), + path ); } }; 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 bf8adc7..c40dbe0 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js @@ -61,10 +61,11 @@ describe("processVariantFiles", () => { `( "when given a variant path ($variantFilePath)", ({ variantFilePath, expectedVariant }) => { + const path = resolve(joinPointFolder, variantFilePath); const variantFiles = [ { name: basename(variantFilePath), - path: resolve(joinPointFolder, variantFilePath) + path } ]; @@ -100,7 +101,7 @@ describe("processVariantFiles", () => { joinPointPath, { pointCut, - variants: [expectedVariant] + variants: new Map([[expectedVariant, path]]) } ] ]) @@ -126,7 +127,7 @@ describe("processVariantFiles", () => { joinPointPath, { pointCut, - variants: [expectedVariant] + variants: new Map([[expectedVariant, path]]) } ] ]) From 027ad216027b18fa8fbb1677631132ecfd1ae28a Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:10:13 +0000 Subject: [PATCH 10/89] serve toggle handlers using Map --- .../serve/src/toggleHandlers/listExtractionFromPathSegment.js | 2 +- .../serve/src/toggleHandlers/singleFilenameDottedSegment.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js b/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js index 6fc9234..91b12d7 100644 --- a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js +++ b/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js @@ -3,7 +3,7 @@ export default ({ togglePoint, joinPoint, variants }) => { const [, , value] = key.split("/"); const list = value.split(","); for (const value of list) { - map.set(value, variants(key)); + map.set(value, variants.get(key)); } return map; }, new Map()); diff --git a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js b/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js index d9dc3bc..7e26117 100644 --- a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js +++ b/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js @@ -2,7 +2,7 @@ export default ({ togglePoint, joinPoint, variants }) => { const featuresMap = new Map(); for (const key of variants.keys()) { const [, , value] = key.split("."); - featuresMap.set(value, variants(key)); + featuresMap.set(value, variants.get(key)); } return togglePoint(joinPoint, featuresMap); }; From e4a16a47f7e7a4d2bb6f5f37868316ffeb83a3c4 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:16:31 +0000 Subject: [PATCH 11/89] update logger for variants Map --- .../src/plugins/togglePointInjection/logger.js | 6 +++--- .../src/plugins/togglePointInjection/logger.test.js | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/logger.js b/packages/webpack/src/plugins/togglePointInjection/logger.js index 07ad8da..2308c53 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.js @@ -16,9 +16,9 @@ class Logger { } ] 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${[ + ...variants.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..cdb4322 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.test.js @@ -21,7 +21,10 @@ describe("logger", () => { describe("logJoinPoints", () => { const pointCut = { name: "test-point-cut" }; const joinPointName = "test-join-point"; - const variants = ["test-variant-1", "test-variant-2"]; + const variants = new Map([ + ["test-key-1", "test-path-1"], + ["test-key-2", "test-key-2"] + ]); const joinPointFiles = new Map([ [ joinPointName, @@ -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( + variants.values() + ).join("\n")}` ); }); }); From b47d992242aa13ef2f42b526df2f5668507c6e24 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:40:00 +0000 Subject: [PATCH 12/89] fixup processVariantFiles --- .../processPointCuts/processVariantFiles/index.js | 12 ++++++------ .../processVariantFiles/index.test.js | 10 ++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js index a296591..3a540ae 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js @@ -2,16 +2,18 @@ import { posix } from "path"; import isJoinPointInvalid from "./isJoinPointInvalid"; const { dirname, relative } = posix; +const normalizeToRelativePath = (path, joinDirectory) => + relative(joinDirectory, path).replace(/^([^./])/, "./$1"); + const processVariantFiles = async ({ variantFiles, joinPointFiles, pointCut, - joinPointResolver, warnings, ...rest }) => { for (const { name, path } of variantFiles) { - const joinPointPath = joinPointResolver(path); + const joinPointPath = pointCut.joinPointResolver(path); const joinDirectory = dirname(joinPointPath); if (!joinPointFiles.has(joinPointPath)) { @@ -39,10 +41,8 @@ const processVariantFiles = async ({ continue; } - joinPointFile.variants.set( - relative(joinDirectory, path).replace(/^([^./])/, "./$1"), - path - ); + const key = normalizeToRelativePath(path, joinDirectory); + joinPointFile.variants.set(key, path); } }; 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 c40dbe0..48bc0f3 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js @@ -5,8 +5,7 @@ const { resolve, basename, join, sep } = posix; 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-*.*"; @@ -30,7 +29,6 @@ describe("processVariantFiles", () => { configFiles, joinPointFiles, pointCut, - joinPointResolver, variantGlob, warnings, name: moduleFile, @@ -71,7 +69,7 @@ describe("processVariantFiles", () => { describe("when given a variant file that has no matching join point file", () => { beforeEach(async () => { - joinPointResolver.mockReturnValue( + pointCut.joinPointResolver.mockReturnValue( join(joinPointFolder, "test-not-matching-control") ); await act({ variantFiles, configFiles: new Map() }); @@ -85,7 +83,7 @@ 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", () => { @@ -93,7 +91,7 @@ describe("processVariantFiles", () => { await act({ variantFiles, configFiles: new Map() }); }); - 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([ From a88b9f727a33e4c41490584575e87c0604649ad4 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:47:04 +0000 Subject: [PATCH 13/89] fixup processPointCuts --- .../processPointCuts/index.js | 7 +++---- .../processPointCuts/index.test.js | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js index 01162c9..8598cf3 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js @@ -10,9 +10,9 @@ const processPointCuts = async ({ const joinPointFiles = new Map(); const configFiles = new Map(); const warnings = []; - for await (const pointCut of pointCuts.values()) { - const { variantGlob, joinPointResolver } = - fillDefaultOptionalValues(pointCut); + for await (const configuredPointCut of pointCuts.values()) { + const pointCut = fillDefaultOptionalValues(configuredPointCut); + const { variantGlob } = pointCut; const variantFiles = await getVariantFiles({ variantGlob, @@ -24,7 +24,6 @@ const processPointCuts = async ({ variantFiles, joinPointFiles, pointCut, - joinPointResolver, variantGlob, warnings, configFiles, diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js index 39ff6bf..c5649e2 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js @@ -8,7 +8,8 @@ jest.mock("./getVariantFiles", () => jest.fn(() => Symbol("test-variant-files")) ); jest.mock("./fillDefaultOptionalValues", () => - jest.fn(() => ({ + jest.fn((pointCut) => ({ + ...pointCut, variantGlob: Symbol("test-variant-glob"), joinPointResolver: Symbol("test-join-point-resolver") })) @@ -16,9 +17,9 @@ jest.mock("./fillDefaultOptionalValues", () => describe("processPointCuts", () => { const pointCuts = new Map([ - ["test-key-1", Symbol("test-point-cut")], - ["test-key-2", Symbol("test-point-cut")], - ["test-key-3", Symbol("test-point-cut")] + ["test-key-1", { ["test-key"]: Symbol("test-point-cut") }], + ["test-key-2", { ["test-key"]: Symbol("test-point-cut") }], + ["test-key-3", { ["test-key"]: Symbol("test-point-cut") }] ]); const pointCutsValues = Array.from(pointCuts.values()); const appRoot = Symbol("test-app-root"); @@ -55,13 +56,15 @@ 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 variantFiles = getVariantFiles.mock.results[index].value; - const { variantGlob, joinPointResolver } = - fillDefaultOptionalValues.mock.results[index].value; + const defaults = fillDefaultOptionalValues.mock.results[index].value; + const { variantGlob } = defaults; expect(processVariantFiles).toHaveBeenCalledWith({ variantFiles, joinPointFiles, - pointCut, - joinPointResolver, + pointCut: { + ...pointCut, + ...defaults + }, variantGlob, warnings, configFiles: expect.any(Map), From c133b09df556c68c4bad4e56d0f85fca53a2efd3 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:07:58 +0000 Subject: [PATCH 14/89] fixup fill optional default values --- .../fillDefaultOptionalValues.js | 9 +- .../fillDefaultOptionalValues.test.js | 84 +++++++------------ 2 files changed, 36 insertions(+), 57 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js index e50b71e..b5c2567 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js @@ -1,13 +1,14 @@ import { posix, basename } from "path"; -const fillPointCutDefaults = (pointCut) => { +const fillDefaultOptionalValues = (pointCut) => { const { variantGlob = "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}", joinPointResolver = (variantPath) => - posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)) + posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)), + loadingMode = "dynamicRequire" } = pointCut; - return { variantGlob, joinPointResolver }; + return { ...pointCut, variantGlob, joinPointResolver, loadingMode }; }; -export default fillPointCutDefaults; +export default fillDefaultOptionalValues; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js index cf5d997..3be6c05 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js @@ -14,61 +14,39 @@ describe("fillDefaultOptionalValues", () => { }); }; - describe("when the point cut has no variantGlob or joinPointResolver", () => { - const pointCut = {}; - - beforeEach(() => { - result = fillPointCutDefaults(pointCut); - }); - - it("should fill the defaults", () => { - expect(result).toEqual({ - variantGlob: "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}", - joinPointResolver: expect.any(Function) - }); - }); - - makeDefaultJoinPointResolverAssertions(); - }); - - describe("when the point cut has a variantGlob but no joinPointResolver", () => { - const pointCut = { variantGlob: Symbol("test-variant-glob") }; - - beforeEach(() => { - result = fillPointCutDefaults(pointCut); - }); - - it("should fill the defaults", () => { - expect(result).toEqual({ - variantGlob: pointCut.variantGlob, - joinPointResolver: expect.any(Function) + const variantGlob = Symbol("test-variant-glob"); + const joinPointResolver = Symbol("test-join-point-resolver"); + const loadingMode = Symbol("test-loading-mode"); + + const defaultVariantGlob = "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"; + const defaultJoinPointResolver = expect.any(Function); + const defaultLoadingMode = "dynamicRequire"; + + describe.each` + variantGlob | joinPointResolver | loadingMode | description | expectation + ${undefined} | ${undefined} | ${undefined} | ${"nothing"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode: defaultLoadingMode }} + ${variantGlob} | ${undefined} | ${undefined} | ${"a variantGlob, but nothing else"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode: defaultLoadingMode }} + ${variantGlob} | ${joinPointResolver} | ${undefined} | ${"a variantGlob and a join point resolver"} | ${{ variantGlob, joinPointResolver, loadingMode: defaultLoadingMode }} + ${variantGlob} | ${undefined} | ${loadingMode} | ${"a variantGlob and a loadingMode"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode }} + ${variantGlob} | ${joinPointResolver} | ${loadingMode} | ${"a variantGlob, a join point resolver and a loadingMode"} | ${{ variantGlob, joinPointResolver, loadingMode }} + ${undefined} | ${joinPointResolver} | ${undefined} | ${"a joinPointResolver, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadingMode: defaultLoadingMode }} + ${undefined} | ${joinPointResolver} | ${loadingMode} | ${"a joinPointResolver and a loadingMode"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadingMode }} + ${undefined} | ${undefined} | ${loadingMode} | ${"a loadingMode, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode }} + `( + "when supplied $description", + // eslint-disable-next-line no-unused-vars + ({ expectation, description, ...pointCut }) => { + beforeEach(() => { + result = fillPointCutDefaults(pointCut); }); - }); - makeDefaultJoinPointResolverAssertions(); - }); - - describe("when the point cut has a joinPointResolver but no variantGlob", () => { - it("should return the supplied joinPointResolver and fill a default variantGlob", () => { - const pointCut = { - joinPointResolver: Symbol("test-join-point-resolver") - }; - const result = fillPointCutDefaults(pointCut); - expect(result).toEqual({ - variantGlob: "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}", - joinPointResolver: pointCut.joinPointResolver + it("should fill the defaults", () => { + expect(result).toEqual(expectation); }); - }); - }); - describe("when the point cut has a variantGlob and a joinPointResolver", () => { - it("should return the point cut supplied values", () => { - const pointCut = { - variantGlob: Symbol("test-variant-glob"), - joinPointResolver: Symbol("test-join-point-resolver") - }; - const result = fillPointCutDefaults(pointCut); - expect(result).toEqual(pointCut); - }); - }); + if (!joinPointResolver) { + makeDefaultJoinPointResolverAssertions(); + } + } + ); }); From 9df38cc40027c9a37b755df28c6990df991ba3de Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:16:10 +0000 Subject: [PATCH 15/89] fix typo --- packages/react-pointcuts/src/useCodeMatches/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-pointcuts/src/useCodeMatches/index.js b/packages/react-pointcuts/src/useCodeMatches/index.js index 6b647b0..3ed7047 100644 --- a/packages/react-pointcuts/src/useCodeMatches/index.js +++ b/packages/react-pointcuts/src/useCodeMatches/index.js @@ -3,7 +3,7 @@ import getMatchedVariant from "./getMatchedVariant"; import getMatchedFeatures from "./getMatchedFeatures"; const useCodeMatches = ({ activeFeatures, variantKey, featuresMap }) => { - const seriazlizedActiveFeatures = JSON.stringify(activeFeatures); + const serializedActiveFeatures = JSON.stringify(activeFeatures); const matches = useMemo(() => { const matchedFeatures = getMatchedFeatures({ @@ -20,7 +20,7 @@ const useCodeMatches = ({ activeFeatures, variantKey, featuresMap }) => { matchedFeatures, matchedVariant }; - }, [seriazlizedActiveFeatures]); // eslint-disable-line react-hooks/exhaustive-deps + }, [serializedActiveFeatures]); // eslint-disable-line react-hooks/exhaustive-deps return matches; }; From 35cb634b4cbb5c9b88d5c83b4b25d6665f399c28 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:16:46 +0000 Subject: [PATCH 16/89] fixup schema? --- packages/webpack/src/plugins/togglePointInjection/schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/schema.json b/packages/webpack/src/plugins/togglePointInjection/schema.json index 17eda73..c077eaf 100644 --- a/packages/webpack/src/plugins/togglePointInjection/schema.json +++ b/packages/webpack/src/plugins/togglePointInjection/schema.json @@ -10,7 +10,7 @@ "instanceof": "Function" }, "loadingMode": { - "anyOf": ["dynamicImport", "dynamicRequire", "static"] + "anyOf": [{ "const": "dynamicImport" }, { "const": "dynamicRequire" }, { "const": "static" }] }, "name": { "type": "string" }, "toggleHandler": { From 59eae9c8d5e575d4cf5891c9d84a9c47e8e5c509 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 17 Mar 2025 22:12:22 +0000 Subject: [PATCH 17/89] update JSDoc args for plugin --- .../src/plugins/togglePointInjection/index.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/index.js b/packages/webpack/src/plugins/togglePointInjection/index.js index 0148828..7741c10 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.js @@ -19,12 +19,17 @@ class TogglePointInjection { * @param {object[]} options.pointCuts toggle point point cut configuration, with target toggle point code as advice. The first matching point cut will be used * @param {string} options.pointCuts[].name name to describe the nature of the point cut, for clarity in logs and dev tools etc. * @param {string} options.pointCuts[].togglePointModule path, from root of the compilation, of where the toggle point sits. Or a resolvable node_module. - * @param {object} [options.pointCuts[].variantGlob=.\/**\/__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 {string} [options.pointCuts[].variantGlob='.\/**\/__variants__/*\/*\/!(*.test).{js,jsx,ts,tsx}'] {@link https://en.wikipedia.org/wiki/Glob_(programming)|Glob} 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.pointsCuts[].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] 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 {'dynamicImport'|'dynamicRequire'|'static'} [options.pointCuts[].loadingMode='dynamicRequire'] module loading mode + * - dynamicImport: Modules are dynamically imported at runtime, returning Promises that resolve to the modules + * - dynamicRequire: Modules are dynamically required at runtime synchronously, returning modules directly + * - static: Modules are statically required at build time (N.B. All side-effects, for all variants, will execute when the parent module is imported) + * @param {string} [options.pointCuts[].webpackMagicComment] A compound {@link https://webpack.js.org/api/module-methods/#magic-comments|Magic Comment} to apply to the dynamic import. Only relevant for loadingMode=dynamicImport. * @param {function} [options.webpackNormalModule] A function that returns 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 + * @example N.B. forward slashes + asterisk are escaped in the examples, due to JSDoc shortcomings, but in reality should be un-escaped * const plugin = new TogglePointInjection({ * pointCuts: [ * { @@ -33,7 +38,9 @@ class TogglePointInjection { * }, * { * togglePointModule: "/withTogglePoint", - * variantGlob: "./**\/!(use*|*.test).{ts,tsx}" + * variantGlob: "./**\/__variants__/*\/*\/!(use*|*.test).tsx", + * loadingMode: "dynamicImport", + * webpackMagicComment: "/* webpackPrefetch *\/" * } * ] * }); From 47294cfe10f5630adf5fe54fffc967bd7c8c5395 Mon Sep 17 00:00:00 2001 From: Tom Pereira <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:31:54 +0000 Subject: [PATCH 18/89] remove nonsense Vary header --- examples/express/src/routes/animals/middleware.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/express/src/routes/animals/middleware.js b/examples/express/src/routes/animals/middleware.js index ef8eae2..ea8e1b7 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.useValue({ value: { version }, scopeCallBack }); }; From b22d7fcc5d48ff090cf87d8e285e958832993a75 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Sun, 27 Apr 2025 19:32:30 +0100 Subject: [PATCH 19/89] package lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1601923..c29970d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@asos/web-toggle-point", - "version": "0.10.3", + "version": "0.10.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@asos/web-toggle-point", - "version": "0.10.3", + "version": "0.10.4", "license": "MIT", "workspaces": [ "packages/features", From 4b41c97111a716fdb4057cfe4d3a2643489d7a3c Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Sun, 11 May 2025 10:32:39 +0100 Subject: [PATCH 20/89] webpack loading schemes --- .gitignore | 3 + package.json | 7 +- packages/webpack/babel.jest.json | 12 ++ packages/webpack/build/rollup.mjs | 37 ++-- packages/webpack/docs/README.md | 14 +- packages/webpack/jest.config.json | 5 + packages/webpack/package.json | 25 +-- packages/webpack/src/index.js | 2 +- ...eferredDynamicImportLoadStrategyFactory.js | 19 +++ ...edDynamicImportLoadStrategyFactory.test.js | 70 ++++++++ .../deferredRequireLoadStrategyFactory.js | 12 ++ ...deferredRequireLoadStrategyFactory.test.js | 71 ++++++++ .../internal/createVariantPathMap.js | 5 + .../internal/dynamicLoadCodeGenerator.js | 10 ++ .../staticLoadStrategyFactory.js | 23 +++ .../staticLoadStrategyFactory.test.js | 76 +++++++++ packages/webpack/src/plugins/index.js | 2 +- .../fillDefaultOptionalValues.js | 32 ++++ .../fillDefaultOptionalValues.test.js | 77 +++++++++ .../src/plugins/togglePointInjection/index.js | 35 ++-- .../togglePointInjection/index.test.js | 81 ++++----- .../togglePointInjection/integration.test.js | 45 ++--- .../plugins/togglePointInjection/logger.js | 4 +- .../togglePointInjection/logger.test.js | 7 +- .../fillDefaultOptionalValues.js | 14 -- .../fillDefaultOptionalValues.test.js | 52 ------ .../processPointCuts/index.js | 6 +- .../processPointCuts/index.test.js | 39 +---- .../processVariantFiles/index.js | 9 +- .../processVariantFiles/index.test.js | 128 +++++++------- .../isJoinPointInvalid.test.js | 6 +- .../processVariantFiles/linkJoinPoints.js | 13 ++ .../linkJoinPoints.test.js | 109 ++++++++++++ .../validateConfigSchema/index.test.js | 7 +- .../handleJoinPointMatch/index.test.js | 5 +- .../resourceProxyExistsInRequestChain.test.js | 1 + .../resolveJoinPoints/index.test.js | 9 +- .../plugins/togglePointInjection/schema.json | 20 +-- .../setupSchemeModules/generateJoinPoint.js | 46 ++--- .../generateJoinPoint.test.js | 161 ++++-------------- .../setupSchemeModules/generatePointCut.js | 18 +- .../generatePointCut.test.js | 79 +++++---- .../setupSchemeModules/index.js | 6 +- .../setupSchemeModules/index.test.js | 21 ++- .../src/toggleHandlerFactories/pathSegment.js | 61 +++++++ .../pathSegment.test.js} | 43 +++-- .../pathSegmentToggleHandler.js | 30 ---- packages/webpack/test/test-utils.js | 1 - 48 files changed, 968 insertions(+), 590 deletions(-) create mode 100644 packages/webpack/babel.jest.json create mode 100644 packages/webpack/jest.config.json create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/internal/createVariantPathMap.js create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/internal/dynamicLoadCodeGenerator.js create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js create mode 100644 packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js delete mode 100644 packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js delete mode 100644 packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.test.js create mode 100644 packages/webpack/src/toggleHandlerFactories/pathSegment.js rename packages/webpack/src/{toggleHandlers/pathSegmentToggleHandler.test.js => toggleHandlerFactories/pathSegment.test.js} (56%) delete mode 100644 packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js diff --git a/.gitignore b/.gitignore index 560b8fd..1d4e2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# Test artifacts +coverage/ + # Dependency directories node_modules/ diff --git a/package.json b/package.json index 540f361..d98b521 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,11 @@ "license": "MIT", "private": true, "keywords": [ - "asos", "toggle point", "feature toggles" ], "engines": { - "node": ">=20" + "node": ">=20.8" }, "scripts": { "build": "npm run build --workspace packages", @@ -23,6 +22,7 @@ "lint:fix": "npm run lint:fix --workspaces --if-present && npm run lint:docs -- --fix && npm run lint:danger -- --fix", "test": "npm run test:unit && npm run test:automation", "test:unit": "npm test --workspace packages", + "test:unit:coverage": "npm test --workspace packages -- --coverage", "test:automation": "npm test --workspace test/automation" }, "devDependencies": { @@ -35,7 +35,6 @@ "@eslint/js": "^9.15.0", "@eslint/markdown": "^6.2.1", "@rollup/plugin-babel": "^6.0.2", - "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-replace": "^5.0.1", "@rollup/plugin-terser": "^0.4.3", @@ -55,8 +54,6 @@ "jsdoc": "^4.0.4", "lint-staged": "^15.2.10", "prettier": "^3.4.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", "rollup": "^3.29.2" }, "workspaces": [ diff --git a/packages/webpack/babel.jest.json b/packages/webpack/babel.jest.json new file mode 100644 index 0000000..984bae4 --- /dev/null +++ b/packages/webpack/babel.jest.json @@ -0,0 +1,12 @@ +{ + "plugins": [ + [ + "babel-plugin-transform-import-meta-x", + { + "replacements": { + "filename": "__filename" + } + } + ] + ] +} diff --git a/packages/webpack/build/rollup.mjs b/packages/webpack/build/rollup.mjs index d85748c..596f9d8 100644 --- a/packages/webpack/build/rollup.mjs +++ b/packages/webpack/build/rollup.mjs @@ -1,33 +1,30 @@ -import pkg from "../package.json" with { type: "json" }; import babel from "@rollup/plugin-babel"; import resolve from "@rollup/plugin-node-resolve"; import external from "rollup-plugin-auto-external"; import commonjs from "@rollup/plugin-commonjs"; import keepExternalComments from "./keepExternalComments.mjs"; -import copy from "rollup-plugin-copy"; import json from "@rollup/plugin-json"; export default { - input: "./src/index.js", - output: [ - { - file: pkg.exports["./plugins"].require, - format: "cjs", - sourcemap: true - }, - { - file: pkg.exports["./plugins"].import, - format: "es", - sourcemap: true - } - ], + input: { + plugins: "./src/plugins/index.js", + "toggleHandlerFactories/pathSegment": + "src/toggleHandlerFactories/pathSegment.js", + "moduleLoadStrategyFactories/staticLoadStrategyFactory": + "src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js", + "moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory": + "src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js", + "moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory": + "src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js" + }, + output: { + dir: "lib/", + entryFileNames: "[name].js", + format: "es", + sourcemap: true + }, plugins: [ keepExternalComments, - copy({ - targets: [ - { src: ["./src/toggleHandlers/*", "!**/*.test.*"], dest: "lib" } - ] - }), babel({ exclude: [/node_modules/], babelHelpers: "runtime" diff --git a/packages/webpack/docs/README.md b/packages/webpack/docs/README.md index f4c7b1d..0ed6c19 100644 --- a/packages/webpack/docs/README.md +++ b/packages/webpack/docs/README.md @@ -36,7 +36,7 @@ import { NormalModule } from 'webpack'; interface PointCut { name: string; - togglePointModule: string; + togglePointModuleSpecifier: string; variantGlob?: string; joinPointResolver?: (variantPath: string) => string; } @@ -53,15 +53,15 @@ interface TogglePointInjectionOptions { > [!IMPORTANT] > N.B. when setting up multiple pointcuts, the path matched by the [globs](https://en.wikipedia.org/wiki/Glob_(programming)) must be mutually exclusive. Otherwise, the pointcut defined earlier in the array "wins", and a warning is emitted into the compilation indicating that the subsequent cuts are neutered for those matching files. > -> Also, due to the way Webpack works, there should only be a sinlge `TogglePointInjection` plugin per webpack configuration, so utilize the point cuts array, rather than having separate plugin instances per point cut. +> Also, due to the way Webpack works, there should only be a single `TogglePointInjectionPlugin` per webpack configuration, so utilize the point cuts array, rather than having separate plugin instances per point cut. #### _`name`_ Each toggling concern should be expressed in the configured `name` of the `PointCut`. This name appears in logs and in dev tools for browser code, but otherwise can be anything. -#### _`togglePointModule`_ +#### _`togglePointModuleSpecifier`_ -The module definition should be resolvable by webpack, either as a path within the codebase, or a module accessible from `node_modules` using webpack module resolution. +The [module specifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#:~:text=The%20module%20specifier%20provides%20a%20string%20that%20the%20JavaScript%20environment%20can%20resolve%20to%20a%20path%20to%20the%20module%20file) should be resolvable by webpack. It's paramount that this module is compatible with the modules it is varying. e.g. @@ -76,7 +76,7 @@ A [glob](https://en.wikipedia.org/wiki/Glob_(programming)) which points at varie This can be as specific or generic as needed, but ideally the most specific possible for the use-cases in effect. -It should match modules that are compatible with the `togglePointModule` - e.g. if all React code is held within a `/components` folder, it makes sense to include this in the glob path to avoid inadvertently toggling non-react code (should a variant be set up for non-React code without considering the configuration). +It should match modules that are compatible with the `togglePointModuleSpecifier` - e.g. if all React code is held within a `/components` folder, it makes sense to include this in the glob path to avoid inadvertently toggling non-react code (should a variant be set up for non-React code without considering the configuration). This glob holds the key for the naming convention approach that underpins the toggle point project, since it is the definition of the join point triggers. @@ -99,7 +99,7 @@ If not supplied, a default `glob` of `/**/__variants__/*/*/!(*.test).{js,jsx,ts, This module unpicks the [WebPack context module](https://webpack.js.org/guides/dependency-management/#context-module-api) produced by enacting the configured `variantGlob` and converts it into a form suitable for the configured `togglePoint`. -If not supplied, a default handler (`@asos/web-toggle-point-webpack/pathSegmentToggleHandler`) is used, compatible with the default `variantGlob`, that converts the matched paths into a tree data structure held in a `Map`, with each path segment as a node in the tree, and the variant modules as the leaf nodes. +If not supplied, a default handler (`@asos/web-toggle-point-webpack/toggleHandlerFactories/pathSegment`) is used, compatible with the default `variantGlob`, that converts the matched paths into a tree data structure held in a `Map`, with each path segment as a node in the tree, and the variant modules as the leaf nodes. #### _`joinPointResolver`_ @@ -162,7 +162,7 @@ Given the following file structure in the repo: const plugin = new TogglePointInjection({ pointCuts: [{ name: "my point cut", - togglePointModule: "/src/modules/withTogglePoint", + togglePointModuleSpecifier: "/src/modules/withTogglePoint", variantGlob: "./src/modules/**/__variants__/*/*/*.js" }] }); diff --git a/packages/webpack/jest.config.json b/packages/webpack/jest.config.json new file mode 100644 index 0000000..9ae7896 --- /dev/null +++ b/packages/webpack/jest.config.json @@ -0,0 +1,5 @@ +{ + "transform": { + "\\.js$": ["babel-jest", { "configFile": "./babel.jest.json" }] + } +} \ No newline at end of file diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 36798ae..429d922 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -1,16 +1,17 @@ { "name": "@asos/web-toggle-point-webpack", - "description": "toggle point webpack plugin", + "description": "toggle point webpack code", "version": "0.7.4", "license": "MIT", "type": "module", - "main": "./lib/main.cjs", + "engines": { + "node": ">=20.11.0" + }, "exports": { - "./plugins": { - "import": "./lib/main.js", - "require": "./lib/main.cjs" - }, - "./pathSegmentToggleHandler": "./lib/pathSegmentToggleHandler.js" + ".": null, + "./plugins": "./lib/plugins.js", + "./moduleLoadStrategyFactories/*": "./lib/moduleLoadStrategyFactories/*.js", + "./toggleHandlerFactories/*": "./lib/toggleHandlerFactories/*.js" }, "repository": { "type": "git", @@ -43,6 +44,7 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.1", "@types/webpack-env": "^1.18.0", + "babel-plugin-transform-import-meta-x": "^0.0.3", "eslint-plugin-jsdoc": "^50.5.0", "jest": "^29.7.0", "jsdoc": "^4.0.2", @@ -50,15 +52,16 @@ "memfs": "^4.15.0", "rimraf": "^6.0.1", "rollup-plugin-auto-external": "^2.0.0", - "rollup-plugin-copy": "^3.5.0", "schema-utils": "^4.2.0", - "webpack": "^5.88.2", + "webpack": "^5.99.7", "webpack-cli": "^4.10.0", "webpack-test-utils": "^2.1.0" }, "peerDependencies": { - "webpack": ">=5.70", - "next": ">14" + "next": ">14", + "react": ">=17", + "react-dom": ">=17", + "webpack": ">=5.70" }, "peerDependenciesMeta": { "next": { diff --git a/packages/webpack/src/index.js b/packages/webpack/src/index.js index a11c1e9..8414418 100644 --- a/packages/webpack/src/index.js +++ b/packages/webpack/src/index.js @@ -4,4 +4,4 @@ import "./external.js"; * Webpack code for injecting toggle points * @module web-toggle-point-webpack */ -export { TogglePointInjection } from "./plugins"; +export { TogglePointInjectionPlugin } from "./plugins"; diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js new file mode 100644 index 0000000..1c5b27d --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js @@ -0,0 +1,19 @@ +import dynamicLoadCodeGenerator from "./internal/dynamicLoadCodeGenerator"; + +export const pack = (expression) => expression; +export const unpack = (expression) => expression(); + +/** + * @param {object} [importCodeGeneratorFactoryOptions] options object + * @param {string} [importCodeGeneratorFactoryOptions.webpackMagicComment] An option webpack magic comment. This is a string that will be added to the import statement, and can be used to control how Webpack handles the import (e.g. prefetching, chunk names etc.). See {@link https://webpack.js.org/api/module-methods/#magic-comments|Webpack Magic Comments} + */ +export default ({ + importCodeGeneratorOptions: { webpackMagicComment } = {} +} = {}) => ({ + adapterModuleSpecifier: import.meta.filename, + importCodeGenerator: dynamicLoadCodeGenerator.bind( + undefined, + "import", + webpackMagicComment + ) +}); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js new file mode 100644 index 0000000..a31bb6d --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js @@ -0,0 +1,70 @@ +import deferredDynamicImportLoadStrategyFactory, { + pack, + unpack +} from "./deferredDynamicImportLoadStrategyFactory.js"; + +const path = "/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, `${path}${relativePath}`]) +); + +describe("deferredDynamicImportLoadStrategyFactory", () => { + let result; + beforeEach(() => { + result = deferredDynamicImportLoadStrategyFactory(); + }); + + it("should return an object containing the adapterModuleSpecifier, indicating the location of the factory code file (so that named exports for the pack and unpack functions can be included in the webpack compilation) and importCodeGenerator", () => { + expect(result).toEqual( + expect.objectContaining({ + adapterModuleSpecifier: expect.stringMatching( + /packages\/webpack\/src\/moduleLoadStrategyFactories\/deferredDynamicImportLoadStrategyFactory\.js$/ + ), + importCodeGenerator: expect.any(Function) + }) + ); + }); + + describe("when the importCodeGenerator is called", () => { + let importCode; + + beforeEach(() => { + importCode = result.importCodeGenerator({ + joinPointPath: path, + variantPathMap + }); + }); + + it("should return a script that prepares a join point function that will dynamically import the join point, when executed", () => { + expect(importCode).toMatch(`const joinPoint = () => import("${path}");`); + }); + + it("should return a script that creates a Map of variants, keyed by relative path, valued as a function that will dynamically import the variant module when executed", () => { + expect(importCode).toMatch(`const variantPathMap = new Map([ + ["/test-sub-folder/test-variant-1", () => import("${path}${relativePaths[0]}")], + ["/test-sub-folder/test-variant-2", () => import("${path}${relativePaths[1]}")], + ["/test-other-sub-folder/test-variant-1", () => import("${path}${relativePaths[2]}")] +]);`); + }); + }); + + describe("pack", () => { + it("should return the expression passed to it", () => { + const expression = Symbol("test"); + expect(pack(expression)).toBe(expression); + }); + }); + + describe("unpack", () => { + it("should call the expression passed to it as a function, and return the result", () => { + const expected = Symbol("test"); + const expression = () => expected; + expect(unpack(expression)).toBe(expected); + }); + }); +}); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js new file mode 100644 index 0000000..e00a9ec --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js @@ -0,0 +1,12 @@ +import dynamicLoadCodeGenerator from "./internal/dynamicLoadCodeGenerator"; + +export const pack = (expression) => expression; +export const unpack = (expression) => expression(); +export default () => ({ + adapterModuleSpecifier: import.meta.filename, + importCodeGenerator: dynamicLoadCodeGenerator.bind( + undefined, + "require", + undefined + ) +}); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js new file mode 100644 index 0000000..8164d96 --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js @@ -0,0 +1,71 @@ +import deferredRequireLoadStrategyFactory, { + pack, + unpack +} from "./deferredRequireLoadStrategyFactory.js"; + +const path = "/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, `${path}${relativePath}`]) +); + +describe("deferredRequireLoadStrategyFactory", () => { + let result; + + beforeEach(() => { + result = deferredRequireLoadStrategyFactory(); + }); + + it("should return an object containing the adapterModuleSpecifier, indicating the location of the factory code file (so that named exports for the pack and unpack functions can be included in the webpack compilation) and importCodeGenerator", () => { + expect(result).toEqual( + expect.objectContaining({ + adapterModuleSpecifier: expect.stringMatching( + /packages\/webpack\/src\/moduleLoadStrategyFactories\/deferredRequireLoadStrategyFactory\.js$/ + ), + importCodeGenerator: expect.any(Function) + }) + ); + }); + + describe("when the importCodeGenerator is called", () => { + let importCode; + + beforeEach(() => { + importCode = result.importCodeGenerator({ + joinPointPath: path, + variantPathMap + }); + }); + + it("should return a script that prepares a join point function that will require the join point, when executed", () => { + expect(importCode).toMatch(`const joinPoint = () => require("${path}");`); + }); + + it("should return a script that creates a Map of variants, keyed by relative path, valued as a function that will require the variant module when executed", () => { + expect(importCode).toMatch(`const variantPathMap = new Map([ + ["/test-sub-folder/test-variant-1", () => require("${path}${relativePaths[0]}")], + ["/test-sub-folder/test-variant-2", () => require("${path}${relativePaths[1]}")], + ["/test-other-sub-folder/test-variant-1", () => require("${path}${relativePaths[2]}")] +]);`); + }); + }); + + describe("pack", () => { + it("should return the expression passed to it", () => { + const expression = Symbol("test"); + expect(pack(expression)).toBe(expression); + }); + }); + + describe("unpack", () => { + it("should call the expression passed to it as a function, and return the result", () => { + const expected = Symbol("test"); + const expression = () => expected; + expect(unpack(expression)).toBe(expected); + }); + }); +}); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/internal/createVariantPathMap.js b/packages/webpack/src/moduleLoadStrategyFactories/internal/createVariantPathMap.js new file mode 100644 index 0000000..770367f --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/internal/createVariantPathMap.js @@ -0,0 +1,5 @@ +const createVariantPathMap = (content) => `const variantPathMap = new Map([ +${content} +]);`; + +export default createVariantPathMap; diff --git a/packages/webpack/src/moduleLoadStrategyFactories/internal/dynamicLoadCodeGenerator.js b/packages/webpack/src/moduleLoadStrategyFactories/internal/dynamicLoadCodeGenerator.js new file mode 100644 index 0000000..7510319 --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/internal/dynamicLoadCodeGenerator.js @@ -0,0 +1,10 @@ +import createVariantPathMap from "./createVariantPathMap"; + +const dynamicLoadCodeGenerator = ( + method, + webpackMagicComment = "", + { joinPointPath, variantPathMap } +) => `const joinPoint = () => ${method}(${webpackMagicComment}"${joinPointPath}"); +${createVariantPathMap([...variantPathMap.keys()].map((key) => ` ["${key}", () => ${method}(${webpackMagicComment}"${variantPathMap.get(key)}")]`).join(",\n"))}`; + +export default dynamicLoadCodeGenerator; diff --git a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js new file mode 100644 index 0000000..13b1e96 --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js @@ -0,0 +1,23 @@ +import createVariantPathMap from "./internal/createVariantPathMap.js"; + +const adapterModuleSpecifier = import.meta.filename; + +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 const pack = (expression) => expression; +export const unpack = (expression) => expression; +export default () => { + return { + adapterModuleSpecifier, + importCodeGenerator + }; +}; diff --git a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js new file mode 100644 index 0000000..416b742 --- /dev/null +++ b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js @@ -0,0 +1,76 @@ +import staticLoadStrategyFactory, { + pack, + unpack +} from "./staticLoadStrategyFactory.js"; + +const path = "/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, `${path}${relativePath}`]) +); + +describe("staticLoadStrategyFactory", () => { + let result; + beforeEach(() => { + result = staticLoadStrategyFactory(); + }); + + it("should return an object containing the adapterModuleSpecifier, indicating the location of the factory code file (so that named exports for the pack and unpack functions can be included in the webpack compilation) and importCodeGenerator", () => { + expect(result).toEqual( + expect.objectContaining({ + adapterModuleSpecifier: expect.stringMatching( + /packages\/webpack\/src\/moduleLoadStrategyFactories\/staticLoadStrategyFactory\.js$/ + ), + importCodeGenerator: expect.any(Function) + }) + ); + }); + + describe("when the importCodeGenerator is called", () => { + let importCode; + + beforeEach(() => { + importCode = result.importCodeGenerator({ + joinPointPath: path, + variantPathMap + }); + }); + + it("should return a script that imports the base / control module for the join point", () => { + expect(importCode).toMatch(`import * as joinPoint from "${path}";`); + }); + + it("should return a script that imports all the valid variants of the base / control module, storing in variables", () => { + expect(importCode).toMatch(` +import * as variant_0 from "${path}${relativePaths[0]}"; +import * as variant_1 from "${path}${relativePaths[1]}"; +import * as variant_2 from "${path}${relativePaths[2]}";`); + }); + + it("should return a script that creates a Map of variants, keyed by relative path, valued as the variant module", () => { + expect(importCode).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] +]);`); + }); + }); + + describe("pack", () => { + it("should return the expression passed to it", () => { + const expression = Symbol("test"); + expect(pack(expression)).toBe(expression); + }); + }); + + describe("unpack", () => { + it("should return the expression passed to it", () => { + const expression = Symbol("test"); + expect(unpack(expression)).toBe(expression); + }); + }); +}); diff --git a/packages/webpack/src/plugins/index.js b/packages/webpack/src/plugins/index.js index 6fb44a0..30e147c 100644 --- a/packages/webpack/src/plugins/index.js +++ b/packages/webpack/src/plugins/index.js @@ -1 +1 @@ -export { default as TogglePointInjection } from "./togglePointInjection/index.js"; +export { default as TogglePointInjectionPlugin } from "./togglePointInjection/index.js"; diff --git a/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js new file mode 100644 index 0000000..f7c1c44 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js @@ -0,0 +1,32 @@ +import { posix, basename } from "path"; +import deferredRequireLoadStrategyFactory from "../../moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js"; + +const defaultLoadStrategy = deferredRequireLoadStrategyFactory(); + +const fillDefaultPointcutValues = (pointCut) => { + const { + variantGlob = "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}", + joinPointResolver = (variantPath) => + posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)), + toggleHandlerFactoryModuleSpecifier = "@asos/web-toggle-point-webpack/toggleHandlerFactories/pathSegment", + loadStrategy = defaultLoadStrategy + } = pointCut; + return { + ...pointCut, + variantGlob, + joinPointResolver, + loadStrategy, + toggleHandlerFactoryModuleSpecifier + }; +}; + +const fillDefaultOptionalValues = (options) => { + return { + webpackNormalModule: async () => + (await import("webpack")).default.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..d18aea7 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js @@ -0,0 +1,77 @@ +import deferredRequireLoadStrategyFactory from "../../moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js"; + +const mockDeferredRequireLoadStrategy = Symbol("test-default-load-strategy"); +jest.mock( + "../../moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js", + () => jest.fn(() => mockDeferredRequireLoadStrategy) +); + +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 variantGlob = Symbol("test-variant-glob"); + const joinPointResolver = Symbol("test-join-point-resolver"); + const loadStrategy = Symbol("test-load-strategy"); + + const defaultVariantGlob = "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"; + const defaultJoinPointResolver = expect.any(Function); + const defaultToggleHandlerFactoryModuleSpecifier = + "@asos/web-toggle-point-webpack/toggleHandlerFactories/pathSegment"; + const toggleHandlerFactoryModuleSpecifier = Symbol( + "test-toggle-handler-factory-module-specifier" + ); + + describe.each` + variantGlob | joinPointResolver | loadStrategy | toggleHandlerFactoryModuleSpecifier | description | expectation + ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${"nothing"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${undefined} | ${undefined} | ${undefined} | ${"a variantGlob, but nothing else"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${joinPointResolver} | ${undefined} | ${undefined} | ${"a variantGlob and a join point resolver"} | ${{ variantGlob, joinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${undefined} | ${loadStrategy} | ${undefined} | ${"a variantGlob and a load strategy"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${joinPointResolver} | ${loadStrategy} | ${undefined} | ${"a variantGlob, a join point resolver and a load strategy"} | ${{ variantGlob, joinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${undefined} | ${joinPointResolver} | ${undefined} | ${undefined} | ${"a joinPointResolver, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${undefined} | ${joinPointResolver} | ${loadStrategy} | ${undefined} | ${"a joinPointResolver and a load strategy"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${undefined} | ${undefined} | ${loadStrategy} | ${undefined} | ${"a load strategy, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: defaultToggleHandlerFactoryModuleSpecifier }} + ${undefined} | ${undefined} | ${undefined} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${undefined} | ${undefined} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier and a variantGlob, but nothing else"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${joinPointResolver} | ${undefined} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier, a variantGlob and a join point resolver"} | ${{ variantGlob, joinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${undefined} | ${loadStrategy} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier, variantGlob and a load strategy"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + ${variantGlob} | ${joinPointResolver} | ${loadStrategy} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier, variantGlob, a join point resolver and a load strategy"} | ${{ variantGlob, joinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + ${undefined} | ${joinPointResolver} | ${undefined} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier and a joinPointResolver, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadStrategy: mockDeferredRequireLoadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + ${undefined} | ${joinPointResolver} | ${loadStrategy} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier, joinPointResolver and a load strategy"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + ${undefined} | ${undefined} | ${loadStrategy} | ${toggleHandlerFactoryModuleSpecifier} | ${"a toggle handler factory module specifier and a load strategy, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadStrategy, toggleHandlerFactoryModuleSpecifier: toggleHandlerFactoryModuleSpecifier }} + `( + "when configuring pointCuts, supplying $description", + // eslint-disable-next-line no-unused-vars + ({ expectation, description, ...pointCut }) => { + beforeEach(async () => { + const { default: fillDefaultOptionalValues } = await import( + "./fillDefaultOptionalValues.js" + ); + result = fillDefaultOptionalValues({ pointCuts: [pointCut] }); + }); + + it("should have retrieved the default load strategy", () => { + expect(deferredRequireLoadStrategyFactory).toHaveBeenCalled(); + }); + + 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 7741c10..0903e34 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.js @@ -6,6 +6,7 @@ import resolveJoinPoints from "./resolveJoinPoints/index.js"; import setupSchemeModules from "./setupSchemeModules/index.js"; import { validate } from "schema-utils"; import schema from "./schema.json"; +import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; /** * Toggle Point Injection Plugin @@ -17,30 +18,30 @@ class TogglePointInjection { * Create a {@link https://webpack.js.org/concepts/plugins/|Plugin} that injects toggle points into a Webpack build * @param {object} options plugin options * @param {object[]} options.pointCuts toggle point point cut configuration, with target toggle point code as advice. The first matching point cut will be used - * @param {string} options.pointCuts[].name name to describe the nature of the point cut, for clarity in logs and dev tools etc. - * @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[].variantGlob='.\/**\/__variants__/*\/*\/!(*.test).{js,jsx,ts,tsx}'] {@link https://en.wikipedia.org/wiki/Glob_(programming)|Glob} 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 {'dynamicImport'|'dynamicRequire'|'static'} [options.pointCuts[].loadingMode='dynamicRequire'] module loading mode - * - dynamicImport: Modules are dynamically imported at runtime, returning Promises that resolve to the modules - * - dynamicRequire: Modules are dynamically required at runtime synchronously, returning modules directly - * - static: Modules are statically required at build time (N.B. All side-effects, for all variants, will execute when the parent module is imported) - * @param {string} [options.pointCuts[].webpackMagicComment] A compound {@link https://webpack.js.org/api/module-methods/#magic-comments|Magic Comment} to apply to the dynamic import. Only relevant for loadingMode=dynamicImport. + * @param {string} options.pointCuts[].name name to describe the nature of the point cut, for clarity in logs and dev tools etc + * @param {string} options.pointCuts[].togglePointModuleSpecifier a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_features_into_your_script|module specifier} pointing to the toggle point module + * @param {string} [options.pointCuts[].variantGlob='.\/**\/__variants__/*\/*\/!(*.test).{js,jsx,ts,tsx}'] {@link https://en.wikipedia.org/wiki/Glob_(programming)|Glob} 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[].toggleHandlerFactoryModuleSpecifier='@asos/web-toggle-point-webpack/toggleHandlerFactories/pathSegment'] 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 factory, that takes a toggle point, and returns a handler that unpicks a Map of relative paths to potential variants, passing that plus a joint point. If not provided, the plugin will use a default handler that processes folder names into a tree held in a Map. The join point and leaf nodes of the tree are modules in a form defined by the loading strategy + * @param {string} [options.pointCuts[].loadStrategy] a module load strategy. default is one created by "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory" * @param {function} [options.webpackNormalModule] A function that returns 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 + asterisk are escaped in the examples, due to JSDoc shortcomings, but in reality should be un-escaped + * import loadStrategyFactory from "@asos/web-toggle-point-react-pointcuts/lazyComponentLoadStrategyFactory"; * const plugin = new TogglePointInjection({ * pointCuts: [ * { - * togglePointModule: "/withToggledHook", + * togglePointModuleSpecifier: "/withToggledHook", * variantGlob: "./**\/__variants__/*\/*\/use!(*.test).{ts,tsx}" * }, * { - * togglePointModule: "/withTogglePoint", + * togglePointModuleSpecifier: "/withTogglePoint", * variantGlob: "./**\/__variants__/*\/*\/!(use*|*.test).tsx", - * loadingMode: "dynamicImport", - * webpackMagicComment: "/* webpackPrefetch *\/" + * loadStrategy: loadStrategyFactory({ + * importCodeGeneratorFactoryOptions: { + * webpackMagicComment: "/* webpackPrefetch *\/" + * } + * }) * } * ] * }); @@ -51,11 +52,7 @@ class TogglePointInjection { */ constructor(options) { validate(schema, options, { name: PLUGIN_NAME, baseDataPath: "options" }); - this.options = { - webpackNormalModule: async () => - (await import("webpack")).default.NormalModule, - ...options - }; + this.options = fillDefaultOptionalValues(options); } apply(compiler) { diff --git a/packages/webpack/src/plugins/togglePointInjection/index.test.js b/packages/webpack/src/plugins/togglePointInjection/index.test.js index 472063b..684b90d 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.test.js @@ -1,12 +1,12 @@ +import schema from "./schema.json"; import processPointCuts from "./processPointCuts/index.js"; -import Logger from "./logger.js"; -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 TogglePointInjectionPlugin from "./index.js"; +import { PLUGIN_NAME } from "./constants.js"; import { validate } from "schema-utils"; -import schema from "./schema.json"; +import Logger from "./logger.js"; jest.mock("./logger.js", () => jest.fn(function () { @@ -20,13 +20,22 @@ 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") })); +const mockNormalModule = Symbol("test-normal-module"); +jest.mock("./fillDefaultOptionalValues.js", () => + jest.fn((options) => ({ + ...options, + webpackNormalModule: jest.fn(() => mockNormalModule) + })) +); jest.mock("schema-utils", () => ({ validate: jest.fn() })); jest.mock("./schema.json", () => Symbol("test-json")); describe("togglePointInjection", () => { let togglePointInjection, compiler, options; - const pointCuts = [{ name: "test-name", togglePointModule: "test-module" }]; + const pointCuts = [ + { [Symbol("test-key")]: Symbol("test-value") }, + { [Symbol("test-key")]: Symbol("test-value") } + ]; beforeEach(() => { jest.clearAllMocks(); @@ -40,7 +49,16 @@ describe("togglePointInjection", () => { }; }); - const makeCommonAssertions = (NormalModule) => { + beforeEach(() => { + options = { pointCuts }; + togglePointInjection = new TogglePointInjectionPlugin(options); + }); + + describe("when applying to a compiler", () => { + beforeEach(() => { + togglePointInjection.apply(compiler); + }); + it("should validate the supplied options", () => { expect(validate).toHaveBeenCalledWith( schema, @@ -49,6 +67,10 @@ describe("togglePointInjection", () => { ); }); + it("should fill in default optional values", () => { + expect(fillDefaultOptionalValues).toHaveBeenCalledWith(options); + }); + it("should tap into the beforeCompile event, indicating the plugin name", () => { expect(compiler.hooks.beforeCompile.tapPromise).toHaveBeenCalledWith( PLUGIN_NAME, @@ -123,9 +145,15 @@ describe("togglePointInjection", () => { ); }); + it("should get the NormalModule from the options", () => { + expect( + togglePointInjection.options.webpackNormalModule + ).toHaveBeenCalled(); + }); + it("should set up scheme modules", () => { expect(setupSchemeModules).toHaveBeenCalledWith({ - NormalModule, + NormalModule: mockNormalModule, compilation, joinPointFiles, pointCuts @@ -162,40 +190,5 @@ describe("togglePointInjection", () => { }); }); }); - }; - - describe("when a webpackNormalModule option is not supplied", () => { - beforeEach(() => { - options = { pointCuts }; - togglePointInjection = new TogglePointInjection(options); - }); - - describe("when applying to a compiler", () => { - beforeEach(() => { - togglePointInjection.apply(compiler); - }); - - makeCommonAssertions(webpack.NormalModule); - }); - }); - - 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"); - - beforeEach(() => { - options = { - pointCuts, - webpackNormalModule: async () => MockNormalModule - }; - togglePointInjection = new TogglePointInjection(options); - }); - - describe("when applying to a compiler", () => { - beforeEach(() => { - togglePointInjection.apply(compiler); - }); - - makeCommonAssertions(MockNormalModule); - }); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/integration.test.js b/packages/webpack/src/plugins/togglePointInjection/integration.test.js index bfa008c..b31e6e5 100644 --- a/packages/webpack/src/plugins/togglePointInjection/integration.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/integration.test.js @@ -1,8 +1,11 @@ import { build } from "webpack-test-utils"; import { readFile } from "fs/promises"; -import { resolve } from "path"; import TogglePointInjection from "./index.js"; import { PLUGIN_NAME } from "./constants.js"; +import { posix } from "path"; +import staticLoadStrategyFactory from "../../moduleLoadStrategyFactories/staticLoadStrategyFactory"; + +const loadStrategy = staticLoadStrategyFactory(); describe("togglePointInjection", () => { let plugin, fileSystem, built; @@ -18,28 +21,27 @@ describe("togglePointInjection", () => { const testCases = [ { name: "not react hooks", - togglePointModule: togglePointModule1, + togglePointModuleSpecifier: togglePointModule1, variantGlob: `${modulesFolder}**/${variantsFolder}/*/*/!(*use)*.js`, - moduleName: "testModule.js" + moduleName: "testModule.js", + loadStrategy }, { name: "react hooks", - togglePointModule: togglePointModule2, + togglePointModuleSpecifier: togglePointModule2, variantGlob: `${modulesFolder}**/${variantsFolder}/*/*/use*.js`, - moduleName: "useTestModule.js" + moduleName: "useTestModule.js", + loadStrategy } ]; beforeEach(async () => { fileSystem = { - "node_modules/@asos/web-toggle-point-webpack/pathSegmentToggleHandler": + "node_modules/@asos/web-toggle-point-webpack/toggleHandlerFactories/pathSegment": await readFile( - resolve( + posix.resolve( __dirname, - "..", - "..", - "toggleHandlers", - "pathSegmentToggleHandler.js" + "../../toggleHandlerFactories/pathSegment.js" ), "utf8" ) @@ -62,7 +64,7 @@ describe("togglePointInjection", () => { ...fileSystem, "/src/index.js": `export { default } from "${modulesFolder}${moduleName}";`, [`${togglePointModule1}.js`]: - "export default (joinPointModule, featuresMap) => ({ joinPointModule, featuresMap })", + "export default ({ joinPoint, featuresMap, unpack }) => ({ joinPoint, featuresMap, unpack })", [`${modulesFolder}${variantsFolder}/${testFeature}/${testVariant}/not-matching-name.js`]: `export default "${variantModuleOutput}";`, [`${modulesFolder}${moduleName}`]: `export default "${baseModuleOutput}";` }, @@ -80,7 +82,7 @@ describe("togglePointInjection", () => { describe.each(testCases)( "when a module is toggled and a matching variant exists", - ({ name, togglePointModule, moduleName }) => { + ({ name, togglePointModuleSpecifier, moduleName }) => { plugin = new TogglePointInjection({ pointCuts: testCases.map(({ moduleName, ...rest }) => rest) // eslint-disable-line no-unused-vars }); @@ -89,8 +91,8 @@ describe("togglePointInjection", () => { { ...fileSystem, "/src/index.js": `export { default } from "${modulesFolder}${moduleName}";`, - [`${togglePointModule}.js`]: - "export default (joinPointModule, featuresMap) => ({ joinPointModule, featuresMap })", + [`${togglePointModuleSpecifier}.js`]: + "export default ({ joinPoint, featuresMap, unpack }) => ({ joinPoint, featuresMap, unpack })", [`${modulesFolder}${variantsFolder}/${testFeature}/${testVariant}/${moduleName}`]: `export default "${variantModuleOutput}";`, [`${modulesFolder}${moduleName}`]: `export default "${baseModuleOutput}";` }, @@ -102,17 +104,18 @@ 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}` ); }); - it("should pass the toggled base module and a Map containing the matched variant to the module at the togglePointModule", () => { + it("should pass the toggled base module and a Map containing the matched variant to the module at the togglePointModuleSpecifier", async () => { const result = built.require("/dist/index.js"); expect(result).toMatchObject({ - joinPointModule: { + joinPoint: { default: baseModuleOutput }, - featuresMap: expect.anything() // jest doesn't have a built-in way to check if an object is a Map + featuresMap: expect.anything(), // jest doesn't have a built-in way to check if an object is a Map + unpack: expect.any(Function) }); const { featuresMap } = result; expect(featuresMap.has(testFeature)).toBe(true); @@ -138,7 +141,7 @@ describe("togglePointInjection", () => { joinPoints: [] }), [`${togglePointModule1}.js`]: - "export default (joinPointModule, featuresMap) => ({ joinPointModule, featuresMap })", + "export default ({ joinPoint, featuresMap, unpack }) => ({ joinPoint, featuresMap, unpack })", [`${modulesFolder}${variantsFolder}/${testFeature}/${testVariant}/${moduleName}`]: `export default "${variantModuleOutput}";`, [`${modulesFolder}${moduleName}`]: `export default "${baseModuleOutput}";` }, @@ -167,7 +170,7 @@ describe("togglePointInjection", () => { ...fileSystem, "/src/index.js": `export { default } from "${modulesFolder}${moduleName}";`, [`${togglePointModule1}.js`]: - "export default (joinPointModule, featuresMap) => ({ joinPointModule, featuresMap })", + "export default ({ joinPoint, featuresMap, unpack }) => ({ joinPoint, featuresMap, unpack })", [`${modulesFolder}${variantsFolder}/${testFeature}/${testVariant}/${moduleName}`]: `export default "${variantModuleOutput}";`, [`${modulesFolder}${moduleName}`]: `export default "${baseModuleOutput}";` }, diff --git a/packages/webpack/src/plugins/togglePointInjection/logger.js b/packages/webpack/src/plugins/togglePointInjection/logger.js index 2308c53..d85b6b5 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.js @@ -11,13 +11,13 @@ 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.values() + ...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 cdb4322..04579f9 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.test.js @@ -1,6 +1,7 @@ import Logger from "./logger"; import { PLUGIN_NAME } from "./constants"; + jest.mock("./constants", () => ({ PLUGIN_NAME: "test-plugin-name" })); @@ -21,7 +22,7 @@ describe("logger", () => { describe("logJoinPoints", () => { const pointCut = { name: "test-point-cut" }; const joinPointName = "test-join-point"; - const variants = new Map([ + const variantPathMap = new Map([ ["test-key-1", "test-path-1"], ["test-key-2", "test-key-2"] ]); @@ -29,7 +30,7 @@ describe("logger", () => { [ joinPointName, { - variants, + variantPathMap, pointCut: { name: "test-point-cut" } } ] @@ -44,7 +45,7 @@ describe("logger", () => { `Identified '${ pointCut.name }' point cut for join point '${joinPointName}' with potential variants:\n${Array.from( - variants.values() + 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 b5c2567..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js +++ /dev/null @@ -1,14 +0,0 @@ -import { posix, basename } from "path"; - -const fillDefaultOptionalValues = (pointCut) => { - const { - variantGlob = "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}", - joinPointResolver = (variantPath) => - posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)), - loadingMode = "dynamicRequire" - } = pointCut; - - return { ...pointCut, variantGlob, joinPointResolver, loadingMode }; -}; - -export default fillDefaultOptionalValues; 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 3be6c05..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js +++ /dev/null @@ -1,52 +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"); - }); - }); - }; - - const variantGlob = Symbol("test-variant-glob"); - const joinPointResolver = Symbol("test-join-point-resolver"); - const loadingMode = Symbol("test-loading-mode"); - - const defaultVariantGlob = "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"; - const defaultJoinPointResolver = expect.any(Function); - const defaultLoadingMode = "dynamicRequire"; - - describe.each` - variantGlob | joinPointResolver | loadingMode | description | expectation - ${undefined} | ${undefined} | ${undefined} | ${"nothing"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode: defaultLoadingMode }} - ${variantGlob} | ${undefined} | ${undefined} | ${"a variantGlob, but nothing else"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode: defaultLoadingMode }} - ${variantGlob} | ${joinPointResolver} | ${undefined} | ${"a variantGlob and a join point resolver"} | ${{ variantGlob, joinPointResolver, loadingMode: defaultLoadingMode }} - ${variantGlob} | ${undefined} | ${loadingMode} | ${"a variantGlob and a loadingMode"} | ${{ variantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode }} - ${variantGlob} | ${joinPointResolver} | ${loadingMode} | ${"a variantGlob, a join point resolver and a loadingMode"} | ${{ variantGlob, joinPointResolver, loadingMode }} - ${undefined} | ${joinPointResolver} | ${undefined} | ${"a joinPointResolver, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadingMode: defaultLoadingMode }} - ${undefined} | ${joinPointResolver} | ${loadingMode} | ${"a joinPointResolver and a loadingMode"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver, loadingMode }} - ${undefined} | ${undefined} | ${loadingMode} | ${"a loadingMode, but nothing else"} | ${{ variantGlob: defaultVariantGlob, joinPointResolver: defaultJoinPointResolver, loadingMode }} - `( - "when supplied $description", - // eslint-disable-next-line no-unused-vars - ({ expectation, description, ...pointCut }) => { - beforeEach(() => { - result = fillPointCutDefaults(pointCut); - }); - - it("should fill the defaults", () => { - expect(result).toEqual(expectation); - }); - - if (!joinPointResolver) { - makeDefaultJoinPointResolverAssertions(); - } - } - ); -}); diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js index 8598cf3..ebeb00e 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 getVariantFiles from "./getVariantFiles.js"; -import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; const processPointCuts = async ({ appRoot, @@ -10,10 +9,8 @@ const processPointCuts = async ({ const joinPointFiles = new Map(); const configFiles = new Map(); const warnings = []; - for await (const configuredPointCut of pointCuts.values()) { - const pointCut = fillDefaultOptionalValues(configuredPointCut); + for await (const pointCut of pointCuts.values()) { const { variantGlob } = pointCut; - const variantFiles = await getVariantFiles({ variantGlob, appRoot, @@ -24,7 +21,6 @@ const processPointCuts = async ({ variantFiles, joinPointFiles, pointCut, - variantGlob, 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 c5649e2..f283116 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js @@ -1,19 +1,11 @@ -import processVariantFiles from "./processVariantFiles/index.js"; -import getVariantFiles from "./getVariantFiles.js"; import processPointCuts from "./index.js"; -import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; +import processVariantFiles from "./processVariantFiles"; +import getVariantFiles from "./getVariantFiles"; -jest.mock("./processVariantFiles/index", () => jest.fn()); -jest.mock("./getVariantFiles", () => +jest.mock("./processVariantFiles/index.js", () => jest.fn()); +jest.mock("./getVariantFiles.js", () => jest.fn(() => Symbol("test-variant-files")) ); -jest.mock("./fillDefaultOptionalValues", () => - jest.fn((pointCut) => ({ - ...pointCut, - variantGlob: Symbol("test-variant-glob"), - joinPointResolver: Symbol("test-join-point-resolver") - })) -); describe("processPointCuts", () => { const pointCuts = new Map([ @@ -22,12 +14,13 @@ describe("processPointCuts", () => { ["test-key-3", { ["test-key"]: Symbol("test-point-cut") }] ]); const pointCutsValues = Array.from(pointCuts.values()); - const appRoot = Symbol("test-app-root"); + const appRoot = "test-app-root"; const fileSystem = Symbol("test-file-system"); let warnings, joinPointFiles; beforeEach(async () => { jest.clearAllMocks(); + ({ warnings, joinPointFiles } = await processPointCuts({ appRoot, fileSystem, @@ -35,16 +28,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 { variantGlob } = - fillDefaultOptionalValues.mock.results[index].value; + for (const { variantGlob } of pointCutsValues.keys()) { expect(getVariantFiles).toHaveBeenCalledWith({ variantGlob, appRoot, @@ -56,16 +41,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 variantFiles = getVariantFiles.mock.results[index].value; - const defaults = fillDefaultOptionalValues.mock.results[index].value; - const { variantGlob } = defaults; expect(processVariantFiles).toHaveBeenCalledWith({ variantFiles, joinPointFiles, - pointCut: { - ...pointCut, - ...defaults - }, - variantGlob, + pointCut, warnings, configFiles: expect.any(Map), fileSystem, @@ -79,7 +58,7 @@ describe("processPointCuts", () => { ).toEqual(1); }); - it("should return an array of warnings an a map of join point files", () => { + it("should return an array of warnings and a Map of join point files", () => { expect(warnings).toBeInstanceOf(Array); expect(joinPointFiles).toBeInstanceOf(Map); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js index 3a540ae..41b3037 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js @@ -1,5 +1,6 @@ import { posix } from "path"; -import isJoinPointInvalid from "./isJoinPointInvalid"; +import isJoinPointInvalid from "./isJoinPointInvalid.js"; +import crossConnectJoinPoints from "./linkJoinPoints.js"; const { dirname, relative } = posix; const normalizeToRelativePath = (path, joinDirectory) => @@ -29,7 +30,7 @@ const processVariantFiles = async ({ } joinPointFiles.set(joinPointPath, { pointCut, - variants: new Map() + variantPathMap: new Map() }); } @@ -42,8 +43,10 @@ const processVariantFiles = async ({ } const key = normalizeToRelativePath(path, joinDirectory); - joinPointFile.variants.set(key, path); + joinPointFile.variantPathMap.set(key, path); } + + crossConnectJoinPoints(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 48bc0f3..2f79808 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js @@ -1,39 +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, basename, 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", joinPointResolver: jest.fn() }; let warnings; const variantFileGlob = "test-variant-*.*"; - const variantGlob = `/${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 ({ variantFiles, configFiles }) => { + const act = async ({ variantFiles }) => { await processVariantFiles({ variantFiles, - configFiles, joinPointFiles, pointCut, - variantGlob, warnings, - name: moduleFile, - fileSystem, - appRoot + ...rest }); }; @@ -48,14 +46,14 @@ describe("processVariantFiles", () => { }); }); - const variantFilePath = variantFileGlob.replaceAll("*", "1"); + const variantFileName = variantFileGlob.replaceAll("*", "1"); describe.each` variantFilePath | expectedVariant - ${variantFilePath} | ${"." + sep + variantFilePath} - ${"." + variantFilePath} | ${"." + variantFilePath} - ${"." + sep + variantFilePath} | ${"." + sep + variantFilePath} - ${".." + sep + variantFilePath} | ${".." + sep + variantFilePath} + ${variantFileName} | ${"." + sep + variantFileName} + ${"." + variantFileName} | ${"." + variantFileName} + ${"." + sep + variantFileName} | ${"." + sep + variantFileName} + ${".." + sep + variantFileName} | ${".." + sep + variantFileName} `( "when given a variant path ($variantFilePath)", ({ variantFilePath, expectedVariant }) => { @@ -67,14 +65,39 @@ describe("processVariantFiles", () => { } ]; - 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 joinPointPath = join( + joinPointFolder, + "test-not-matching-control" + ); + beforeEach(async () => { - pointCut.joinPointResolver.mockReturnValue( - join(joinPointFolder, "test-not-matching-control") - ); + pointCut.joinPointResolver.mockReturnValue(joinPointPath); + isJoinPointInvalid.mockReturnValue(true); await act({ variantFiles, configFiles: new Map() }); }); + makeCommonAssertions(); + + it("should call isJoinPointInvalid with the expected arguments", () => { + expect(isJoinPointInvalid).toHaveBeenCalledWith({ + name: variantFiles[0].name, + joinPointPath, + joinDirectory: joinPointFolder, + ...rest + }); + }); + it("should add no warnings, and not modify joinPointFiles", async () => { expect(warnings).toEqual([]); expect(joinPointFiles).toEqual(new Map()); @@ -88,36 +111,22 @@ describe("processVariantFiles", () => { describe("and no config file precludes it being valid", () => { beforeEach(async () => { - await act({ variantFiles, configFiles: new Map() }); + isJoinPointInvalid.mockReturnValue(false); + await act({ variantFiles }); }); - 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([ - [ - joinPointPath, - { - pointCut, - variants: new Map([[expectedVariant, path]]) - } - ] - ]) - ); - }); - }); + makeCommonAssertions(); - describe("and a config file confirms it as valid", () => { - beforeEach(async () => { - await act({ - variantFiles, - configFiles: new Map([ - [joinPointFolder, { joinPoints: [moduleFile] }] - ]) + it("should call isJoinPointInvalid with the expected arguments", () => { + expect(isJoinPointInvalid).toHaveBeenCalledWith({ + name: variantFiles[0].name, + joinPointPath, + joinDirectory: 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([ @@ -125,7 +134,7 @@ describe("processVariantFiles", () => { joinPointPath, { pointCut, - variants: new Map([[expectedVariant, path]]) + variantPathMap: new Map([[expectedVariant, path]]) } ] ]) @@ -133,20 +142,6 @@ describe("processVariantFiles", () => { }); }); - describe("and a config file precludes it from being valid", () => { - beforeEach(async () => { - await act({ - variantFiles, - 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 () => { @@ -154,11 +149,16 @@ describe("processVariantFiles", () => { pointCut: testOtherPointCut }); await act({ - variantFiles, - configFiles: new Map() + variantFiles }); }); + 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/isJoinPointInvalid.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/isJoinPointInvalid.test.js index 1cc0804..1ec12a5 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/isJoinPointInvalid.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/isJoinPointInvalid.test.js @@ -1,8 +1,8 @@ -import { TOGGLE_CONFIG } from "../../constants"; -import isJoinPointInvalid from "./isJoinPointInvalid"; import { memfs } from "memfs"; -import validateConfigSchema from "./validateConfigSchema"; import { join } from "path"; +import validateConfigSchema from "./validateConfigSchema"; +import { TOGGLE_CONFIG } from "../../constants"; +import isJoinPointInvalid from "./isJoinPointInvalid"; jest.mock("../../constants", () => ({ TOGGLE_CONFIG: "test-toggle-config-filename" 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/processPointCuts/processVariantFiles/validateConfigSchema/index.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/validateConfigSchema/index.test.js index 8899431..bae1c46 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/validateConfigSchema/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/validateConfigSchema/index.test.js @@ -1,11 +1,12 @@ -import validateConfigSchema from "./index"; +import validateConfigSchema from "."; import { PLUGIN_NAME } from "../../../constants"; import { validate } from "schema-utils"; +import configSchema from "./configSchema.json"; jest.mock("schema-utils", () => ({ validate: jest.fn() })); -jest.mock("./configSchema.json", () => ({})); +jest.mock("./configSchema.json", () => Symbol("test-config-schema")); jest.mock("../../../constants", () => ({ PLUGIN_NAME: "test-plugin-name" })); @@ -20,7 +21,7 @@ describe("validateConfigSchema", () => { }); it("should validate the schema of the config file, and output with the plugin name and 'toggle config' as the data path that errored", () => { - expect(validate).toHaveBeenCalledWith({}, configFile, { + expect(validate).toHaveBeenCalledWith(configSchema, configFile, { name: PLUGIN_NAME, baseDataPath: "toggle config", postFormatter: expect.any(Function) diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/index.test.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/index.test.js index 34ebd7e..70e9ad7 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/index.test.js @@ -1,7 +1,7 @@ import handleJoinPointMatch from "."; -import { SCHEME, JOIN_POINTS } from "../../constants"; import getIssuerModule from "./getIssuerModule"; import resourceProxyExistsInRequestChain from "./resourceProxyExistsInRequestChain"; +import { SCHEME, JOIN_POINTS } from "../../constants"; jest.mock("./getIssuerModule", () => jest.fn()); jest.mock("./resourceProxyExistsInRequestChain", () => jest.fn()); @@ -18,8 +18,9 @@ describe("handleJoinPointMatch", () => { const mockOriginalRequest = Symbol("test-original-request"); let resolveData; - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); + resolveData = { request: mockOriginalRequest }; 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..7250af3 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js @@ -1,6 +1,7 @@ import resourceProxyExistsInRequestChain from "./resourceProxyExistsInRequestChain"; import { createMockGraph } from "../../../../../test/test-utils"; + const moduleGraph = { getIncomingConnections: jest.fn() }; const proxyResource = Symbol("test-proxy-resource"); diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js index e9628c8..9917b8c 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,8 @@ describe("resolveJoinPoints", () => { beforeEach(() => { [, beforeResolveCallback] = normalModuleFactory.hooks.beforeResolve.tapPromise.mock.lastCall; - handleJoinPointMatch.mockClear(); + // handleJoinPointMatch.mockClear(); + jest.clearAllMocks(); beforeResolveCallback(); }); @@ -77,7 +78,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, diff --git a/packages/webpack/src/plugins/togglePointInjection/schema.json b/packages/webpack/src/plugins/togglePointInjection/schema.json index c077eaf..c51c921 100644 --- a/packages/webpack/src/plugins/togglePointInjection/schema.json +++ b/packages/webpack/src/plugins/togglePointInjection/schema.json @@ -9,28 +9,14 @@ "joinPointResolver": { "instanceof": "Function" }, - "loadingMode": { - "anyOf": [{ "const": "dynamicImport" }, { "const": "dynamicRequire" }, { "const": "static" }] - }, "name": { "type": "string" }, - "toggleHandler": { + "toggleHandlerFactoryModuleSpecifier": { "type": "string" }, - "togglePointModule": { "type": "string" }, + "togglePointModuleSpecifier": { "type": "string" }, "variantGlob": { "type": "string" } }, - "if": { - "properties": { - "loadingMode": { "const": "dynamicImport" } - } - }, - "then": { - "properties": { - "webpackMagicComment": { "type": "string" } - } - }, - "additionalProperties": false, - "required": ["name", "togglePointModule"] + "required": ["name", "togglePointModuleSpecifier"] } }, "webpackNormalModule": { diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js index 169abbc..d212738 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js @@ -1,44 +1,20 @@ import { POINT_CUTS, SCHEME } from "../constants.js"; -const getStatic = ({ path, variants }) => { - const variantsKeys = Array.from(variants.keys()); - const code = `import * as joinPoint from "${path}"; -${variantsKeys - .map( - (key, index) => `import * as variant_${index} from "${variants.get(key)}";` - ) - .join("\n")} -const variants = new Map([ -${variantsKeys.map((relativePath, index) => ` ["${relativePath}", variant_${index}]`).join(",\n")} -]);`; - - return code; -}; - -const getDynamic = (method, webpackMagicComment, { path, variants }) => { - const variantsKeys = Array.from(variants.keys()); - const code = `const joinPoint = () => ${method}(${webpackMagicComment}"${path}"); -const variants = new Map([ -${variantsKeys.map((key) => ` ["${key}", () => ${method}(${webpackMagicComment}"${variants.get(key)}")]`).join(",\n")} -]);`; - - return code; -}; - -const generateJoinPoint = ({ joinPointFiles, path }) => { +const generateJoinPoint = ({ joinPointFiles, joinPointPath }) => { const { - pointCut: { name, loadingMode, webpackMagicComment = "" }, - variants - } = joinPointFiles.get(path); + pointCut: { + name, + loadStrategy: { importCodeGenerator } + }, + variantPathMap + } = joinPointFiles.get(joinPointPath); const pointCutImport = `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}";`; - const code = { - dynamicImport: getDynamic.bind(undefined, "import", webpackMagicComment), - dynamicRequire: getDynamic.bind(undefined, "require", ""), - static: getStatic - }[loadingMode]({ path, variants }); + + const code = importCodeGenerator({ joinPointPath, variantPathMap }); + return `${pointCutImport} ${code} -export default pointCut({ joinPoint, variants });`; +export default pointCut({ joinPoint, variantPathMap });`; }; 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 index f9baf4c..4e5b41f 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js @@ -1,149 +1,52 @@ 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 joinPointPath = "/test-path"; const pointCutName = "test-point-cut"; - const relativePaths = [ - "/test-sub-folder/test-variant-1", - "/test-sub-folder/test-variant-2", - "/test-other-sub-folder/test-variant-1" - ]; - const variants = new Map( - relativePaths.map((relativePath) => [ - relativePath, - `${path}${relativePath}` - ]) - ); - const pointCut = { name: pointCutName }; - let result; - - const makeCommonAssertions = () => { - 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 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 });" - ); - }); + const variantPathMap = Symbol("test-variant-path-map"); + const mockImportCode = + "const joinPoint = 'test-join-point'; const variantPathMap = 'test-variants';"; + const importCodeGenerator = jest.fn(() => mockImportCode); + const pointCut = { + name: pointCutName, + loadStrategy: { importCodeGenerator } }; + let result; - describe("with a static loadingMode", () => { - beforeEach(() => { - const joinPointFiles = new Map([ - [path, { pointCut: { ...pointCut, loadingMode: "static" }, variants }] - ]); - result = generateJoinPoint({ - joinPointFiles, - path - }); - }); - - makeCommonAssertions(); - - it("should return a script that statically 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, storing in variables", () => { - expect(result).toMatch(` -import * as variant_0 from "${path}${relativePaths[0]}"; -import * as variant_1 from "${path}${relativePaths[1]}"; -import * as variant_2 from "${path}${relativePaths[2]}";`); - }); - - it("should return a script that creates a Map of variants, keyed by relative path, valued as the variant module", () => { - expect(result).toMatch(`const variants = 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] -]);`); + beforeEach(() => { + const joinPointFiles = new Map([ + [joinPointPath, { pointCut, variantPathMap }] + ]); + result = generateJoinPoint({ + joinPointFiles, + joinPointPath }); }); - describe.each(["/* test loading qualifier */", undefined])( - "with a dynamicImport loadingMode, and a %s webpackMagicComment", - (webpackMagicComment) => { - const expectedMagicComment = webpackMagicComment ?? ""; - beforeEach(() => { - const joinPointFiles = new Map([ - [ - path, - { - pointCut: { - ...pointCut, - loadingMode: "dynamicImport", - webpackMagicComment - }, - variants - } - ] - ]); - result = generateJoinPoint({ - joinPointFiles, - path - }); - }); - - makeCommonAssertions(); - - it("should return a script that prepares a join point function that will dynamically load the join point, when executed, with any provided webpack loading directives", () => { - expect(result).toMatch( - `const joinPoint = () => import(${expectedMagicComment}"${path}");` - ); - }); - - it("should return a script that creates a Map of variants, keyed by relative path, valued as a function that will dynamically load the variant module when executed, with any provided webpack loading directives", () => { - expect(result).toMatch(`const variants = new Map([ - ["/test-sub-folder/test-variant-1", () => import(${expectedMagicComment}"${path}${relativePaths[0]}")], - ["/test-sub-folder/test-variant-2", () => import(${expectedMagicComment}"${path}${relativePaths[1]}")], - ["/test-other-sub-folder/test-variant-1", () => import(${expectedMagicComment}"${path}${relativePaths[2]}")] -]);`); - }); - } - ); - - describe("with a dynamicRequire loadingMode", () => { - beforeEach(() => { - const joinPointFiles = new Map([ - [ - path, - { - pointCut: { - ...pointCut, - loadingMode: "dynamicRequire" - }, - variants - } - ] - ]); - result = generateJoinPoint({ - joinPointFiles, - path - }); - }); - - makeCommonAssertions(); + 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 prepares a join point function that will dynamically load the join point, when executed", () => { - expect(result).toMatch(`const joinPoint = () => require("${path}");`); + 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 creates a Map of variants, keyed by relative path, valued as a function that will dynamically load the variant module when executed", () => { - expect(result).toMatch(`const variants = new Map([ - ["/test-sub-folder/test-variant-1", () => require("${path}${relativePaths[0]}")], - ["/test-sub-folder/test-variant-2", () => require("${path}${relativePaths[1]}")], - ["/test-other-sub-folder/test-variant-1", () => require("${path}${relativePaths[2]}")] -]);`); - }); + 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..64891af 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js @@ -1,13 +1,15 @@ -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" + togglePointModuleSpecifier, + toggleHandlerFactoryModuleSpecifier, + loadStrategy: { adapterModuleSpecifier } } = pointCuts.find(({ name }) => name === pointCutName); - - return `import togglePoint from "${togglePointModule}"; -import handler from "${toggleHandler}"; -export default (rest) => handler({ togglePoint, ...rest });`; + return `import togglePoint from "${togglePointModuleSpecifier}"; +import handlerFactory from "${toggleHandlerFactoryModuleSpecifier}"; +import { pack, unpack } from "${adapterModuleSpecifier}"; +const handler = handlerFactory({ togglePoint, pack, unpack }); +export default handler;`; }; export default generatePointCut; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js index cbcc7d6..da9a5a4 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js @@ -2,55 +2,62 @@ import generatePointCut from "./generatePointCut.js"; describe("generatePointCut", () => { const pointCutName = "test-point-cut"; - const path = `/${pointCutName}`; - const togglePointModule = "test-toggle-point-path"; + const joinPointPath = `/${pointCutName}`; + const togglePointModuleSpecifier = "test-toggle-point-path"; + const toggleHandlerFactoryModuleSpecifier = + "test-toggle-handler-factory-module-specifier"; + const adapterModuleSpecifier = "test-adapter-module-specifier"; let result, pointCuts; beforeEach(() => { pointCuts = [ - { name: "test-other-point-cut" }, - { name: pointCutName, togglePointModule } + { name: "test-other-point-cut", toggleHandlerFactoryModuleSpecifier }, + { + name: pointCutName, + togglePointModuleSpecifier, + toggleHandlerFactoryModuleSpecifier, + loadStrategy: { + adapterModuleSpecifier + } + } ]; }); - const makeCommonAssertions = () => { - it("should return a script that imports the appropriate toggle point", () => { - expect(result).toMatch(`import togglePoint from "${togglePointModule}";`); - }); - - it("should return a script exports a default export which calls the toggle handler, passing the toggle point and any other properties of the first argument given to it", () => { - expect(result).toMatch( - "export default (rest) => handler({ togglePoint, ...rest });" - ); - }); - }; - - describe("when a toggle handler is configured against the point cut", () => { - const toggleHandler = "test-toggle-handler"; + beforeEach(() => { + result = generatePointCut({ pointCuts, joinPointPath }); + }); - beforeEach(() => { - pointCuts[1].toggleHandler = toggleHandler; - result = generatePointCut({ pointCuts, path }); - }); + it("should return a script that imports the appropriate toggle point", () => { + expect(result).toMatch( + `import togglePoint from "${togglePointModuleSpecifier}";` + ); + }); - it("should return a script that imports the appropriate toggle handler", () => { - expect(result).toMatch(`import handler from "${toggleHandler}";`); - }); + it("should return a script that imports the appropriate toggle handler factory", () => { + expect(result).toMatch( + `import handlerFactory from "${toggleHandlerFactoryModuleSpecifier}";` + ); + }); - makeCommonAssertions(); + it("should return a script that imports the pack and unpack exports of the appropriate adapter module", () => { + expect(result).toMatch( + `import { pack, unpack } from "${adapterModuleSpecifier}";` + ); }); - describe("when a toggle handler is not configured against the point cut", () => { - beforeEach(() => { - result = generatePointCut({ pointCuts, path }); - }); + it("should return a script that constructs a toggle handler, passing the toggle point to the factory, plus the pack and unpack functions from the load strategy adapter", () => { + expect(result).toMatch( + "const handler = handlerFactory({ togglePoint, pack, unpack });" + ); + }); - it("should return a script that imports the default toggle handler (a path segment toggle handler)", () => { - expect(result).toMatch( - `import handler from "@asos/web-toggle-point-webpack/pathSegmentToggleHandler";` - ); - }); + it("should return a script with a handler default export", () => { + expect(result).toMatch("export default handler;"); + }); - makeCommonAssertions(); + it("should return a script that imports the appropriate toggle handler", () => { + expect(result).toMatch( + `import handlerFactory from "${toggleHandlerFactoryModuleSpecifier}";` + ); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js index b5d014f..2f2508e 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js @@ -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..692cc1c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js @@ -24,7 +24,7 @@ describe("setupSchemeModules", () => { const joinPointFiles = Symbol("test-join-point-files"); const pointCuts = Symbol("test-point-cuts"); - beforeEach(() => { + beforeEach(async () => { setupSchemeModules({ NormalModule, compilation, @@ -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/toggleHandlerFactories/pathSegment.js b/packages/webpack/src/toggleHandlerFactories/pathSegment.js new file mode 100644 index 0000000..7124ccb --- /dev/null +++ b/packages/webpack/src/toggleHandlerFactories/pathSegment.js @@ -0,0 +1,61 @@ +const buildTree = (map = new Map(), parts, value) => { + const [part, ...rest] = parts; + if (rest.length) { + map.set(part, buildTree(map.get(part), rest, value)); + } else { + map.set(part, value); + } + return map; +}; + +/** + * Path Segment Toggle Handler Factory + * @memberof module:web-toggle-point-webpack + * @inner + * @param {object} options toggle handler factory options + * @param {function} options.togglePoint a method that chooses the appropriate module at runtime, passed a join points and a Map of feature keys to variants + * @param {function} options.pack a method to pack a module, as returned by the join point, in preparation for use by the toggle point. must be a named export of the loading strategy, but can be an identity function + * @param {function} options.unpack a method to unpack a module when needed by the toggle point. must be a named export of the loading strategy, but can be an identity function + * @returns {module:web-toggle-point-webpack.pathSegmentToggleHandler} a toggle handler that takes a join point and a Map of feature keys to variants, and returns a module + * @example + * const pathSegmentToggleHandler = pathSegmentToggleHandlerFactory({ + * togglePoint: ({ joinPoint, featuresMap, unpack }) => { + * const choseFeature = ... // toggle point logic, picking either the packed join point or a variant module + * return unpack(chosenFeature); + * }), + * pack: (moduleNamespace) => moduleNamespace(), + * unpack: (moduleNamespace) => moduleNamespace().default + * }); + */ +const pathSegmentToggleHandlerFactory = ({ togglePoint, pack, unpack }) => { + /** + * Path Segment Toggle Handler + * @function pathSegmentToggleHandler + * @static + * @memberof module:web-toggle-point-webpack + * @param {object} params handler parameters + * @param {module} params.joinPoint the join point + * @param {Map} params.variantPathMap a Map of posix file paths, relative to the join point module, valued in a form defined by the loading strategy + * @returns {function} A handler of join points injected by the plugin + * @example + * const result = pathSegmentToggleHandler({ + * joinPoint: () => import("/src/some-base-module.js"), + * variantPathMap: new Map([ + * ["/src/variants/some-variant-1.js", () => import("/src/variants/some-variant-1.js")], + * ["/src/variants/some-variant-2.js", () => import("/src/variants/some-variant-2.js")], + * ]), + * }); + */ + const pathSegmentToggleHandler = ({ joinPoint, variantPathMap }) => { + let featuresMap; + for (const [key, value] of variantPathMap) { + const parts = key.split("/").slice(0, -1).slice(2); + featuresMap = buildTree(featuresMap, parts, pack(value)); + } + return togglePoint({ joinPoint: pack(joinPoint), featuresMap, unpack }); + }; + + return pathSegmentToggleHandler; +}; + +export default pathSegmentToggleHandlerFactory; diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js b/packages/webpack/src/toggleHandlerFactories/pathSegment.test.js similarity index 56% rename from packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js rename to packages/webpack/src/toggleHandlerFactories/pathSegment.test.js index d13217f..d94926f 100644 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js +++ b/packages/webpack/src/toggleHandlerFactories/pathSegment.test.js @@ -1,23 +1,33 @@ -import pathSegmentToggleHandler from "./pathSegmentToggleHandler.js"; +import pathSegmentToggleHandlerFactory from "./pathSegment.js"; + const toggleOutcome = Symbol("test-outcome"); const togglePoint = jest.fn(() => toggleOutcome); -const joinPoint = Symbol("mock-join-point"); +const pack = jest.fn(() => Symbol("packed")); +const unpack = jest.fn(() => Symbol("unpacked")); -describe("pathSegmentToggleHandler", () => { - let result, variants; +describe("toggleHandlerFactories/pathSegment", () => { + let toggleHandlerFactory; beforeEach(() => { jest.clearAllMocks(); + toggleHandlerFactory = pathSegmentToggleHandlerFactory({ + togglePoint, + pack, + unpack + }); }); [1, 2, 3].forEach((segmentCount) => { const keyArray = [...Array(segmentCount).keys()]; describe(`given a Map keyed by variant paths with ${segmentCount} path segments (after the variants path)`, () => { + let result, variantPathMap; + const joinPoint = Symbol("mock-join-point"); + beforeEach(() => { const segments = keyArray.map((key) => `test-segment-${key}/`); - variants = new Map([ + variantPathMap = new Map([ [ `./__variants__/${segments.join("")}test-variant.js`, Symbol("test-variant") @@ -27,11 +37,23 @@ describe("pathSegmentToggleHandler", () => { Symbol("test-variant") ] ]); - result = pathSegmentToggleHandler({ togglePoint, joinPoint, variants }); + result = toggleHandlerFactory({ + joinPoint, + variantPathMap + }); }); - it("should call the toggle point with the join point module and a map", () => { - expect(togglePoint).toHaveBeenCalledWith(joinPoint, expect.any(Map)); + it("should pack the join point", () => { + expect(pack).toHaveBeenCalledWith(joinPoint); + }); + + it("should call the toggle point with the join point module and a map, plus the passed method to unpack modules", () => { + const packedJoinPoint = pack.mock.results.pop().value; + expect(togglePoint).toHaveBeenCalledWith({ + joinPoint: packedJoinPoint, + featuresMap: expect.any(Map), + unpack + }); }); it("should return the outcome of the toggle point", () => { @@ -46,14 +68,15 @@ 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(variants)) { + 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(variants.get(key)); + expect(pack).toHaveBeenCalledWith(variantPathMap.get(key)); + expect(node).toBe(pack.mock.results.pop().value); } }); }); diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js deleted file mode 100644 index 5aac6a0..0000000 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js +++ /dev/null @@ -1,30 +0,0 @@ -const buildTree = (map = new Map(), parts, variants, key) => { - const [part, ...rest] = parts; - if (rest.length) { - map.set(part, buildTree(map.get(part), rest, variants, key)); - } else { - map.set(part, variants.get(key)); - } - return map; -}; - -/** - * Path Segment Toggle Handler - * @memberof module:web-toggle-point-webpack - * @inner - * @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 {Map} options.variants a Map of posix relative file paths to variant modules - * @returns {function} A handler of join points injected by the plugin - */ -const pathSegmentToggleHandler = ({ togglePoint, joinPoint, variants }) => { - let featuresMap; - for (const key of variants.keys()) { - const parts = key.split("/").slice(0, -1).slice(2); - featuresMap = buildTree(featuresMap, parts, variants, key); - } - return togglePoint(joinPoint, featuresMap); -}; - -export default pathSegmentToggleHandler; 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) => ({ From 66657babdf9a57deab5038aed8670032df1928c0 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 12 May 2025 21:04:58 +0100 Subject: [PATCH 21/89] make pack and unpack optional --- .../deferredDynamicImportLoadStrategyFactory.js | 1 - ...ferredDynamicImportLoadStrategyFactory.test.js | 12 ++++-------- .../deferredRequireLoadStrategyFactory.js | 1 - .../deferredRequireLoadStrategyFactory.test.js | 12 ++++-------- .../staticLoadStrategyFactory.js | 2 -- .../staticLoadStrategyFactory.test.js | 15 +++++---------- .../setupSchemeModules/generatePointCut.js | 6 ++++-- .../setupSchemeModules/generatePointCut.test.js | 9 ++++++--- 8 files changed, 23 insertions(+), 35 deletions(-) diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js index 1c5b27d..61f63e0 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.js @@ -1,6 +1,5 @@ import dynamicLoadCodeGenerator from "./internal/dynamicLoadCodeGenerator"; -export const pack = (expression) => expression; export const unpack = (expression) => expression(); /** diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js index a31bb6d..6937fa0 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js @@ -1,7 +1,4 @@ -import deferredDynamicImportLoadStrategyFactory, { - pack, - unpack -} from "./deferredDynamicImportLoadStrategyFactory.js"; +import deferredDynamicImportLoadStrategyFactory, * as namespace from "./deferredDynamicImportLoadStrategyFactory.js"; const path = "/test-folder/test-path"; const relativePaths = [ @@ -54,9 +51,8 @@ describe("deferredDynamicImportLoadStrategyFactory", () => { }); describe("pack", () => { - it("should return the expression passed to it", () => { - const expression = Symbol("test"); - expect(pack(expression)).toBe(expression); + it("should not export a pack function, so that the default (identity function) is used", () => { + expect(namespace.pack).toBe(undefined); // eslint-disable-line import/namespace }); }); @@ -64,7 +60,7 @@ describe("deferredDynamicImportLoadStrategyFactory", () => { it("should call the expression passed to it as a function, and return the result", () => { const expected = Symbol("test"); const expression = () => expected; - expect(unpack(expression)).toBe(expected); + expect(namespace.unpack(expression)).toBe(expected); }); }); }); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js index e00a9ec..edc5d2c 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.js @@ -1,6 +1,5 @@ import dynamicLoadCodeGenerator from "./internal/dynamicLoadCodeGenerator"; -export const pack = (expression) => expression; export const unpack = (expression) => expression(); export default () => ({ adapterModuleSpecifier: import.meta.filename, diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js index 8164d96..a3c6e7e 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js @@ -1,7 +1,4 @@ -import deferredRequireLoadStrategyFactory, { - pack, - unpack -} from "./deferredRequireLoadStrategyFactory.js"; +import deferredRequireLoadStrategyFactory, * as namespace from "./deferredRequireLoadStrategyFactory.js"; const path = "/test-folder/test-path"; const relativePaths = [ @@ -55,9 +52,8 @@ describe("deferredRequireLoadStrategyFactory", () => { }); describe("pack", () => { - it("should return the expression passed to it", () => { - const expression = Symbol("test"); - expect(pack(expression)).toBe(expression); + it("should not export a pack function, so that the default (identity function) is used", () => { + expect(namespace.pack).toBe(undefined); // eslint-disable-line import/namespace }); }); @@ -65,7 +61,7 @@ describe("deferredRequireLoadStrategyFactory", () => { it("should call the expression passed to it as a function, and return the result", () => { const expected = Symbol("test"); const expression = () => expected; - expect(unpack(expression)).toBe(expected); + expect(namespace.unpack(expression)).toBe(expected); }); }); }); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js index 13b1e96..ae71696 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.js @@ -13,8 +13,6 @@ ${variantsKeys .join("\n")} ${createVariantPathMap(variantsKeys.map((key, index) => ` ["${key}", variant_${index}]`).join(",\n"))}`; }; -export const pack = (expression) => expression; -export const unpack = (expression) => expression; export default () => { return { adapterModuleSpecifier, diff --git a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js index 416b742..05c3f78 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js @@ -1,7 +1,4 @@ -import staticLoadStrategyFactory, { - pack, - unpack -} from "./staticLoadStrategyFactory.js"; +import staticLoadStrategyFactory, * as namespace from "./staticLoadStrategyFactory.js"; const path = "/test-folder/test-path"; const relativePaths = [ @@ -61,16 +58,14 @@ import * as variant_2 from "${path}${relativePaths[2]}";`); }); describe("pack", () => { - it("should return the expression passed to it", () => { - const expression = Symbol("test"); - expect(pack(expression)).toBe(expression); + it("should not export a pack function, so that the default (identity function) is used", () => { + expect(namespace.pack).toBe(undefined); // eslint-disable-line import/namespace }); }); describe("unpack", () => { - it("should return the expression passed to it", () => { - const expression = Symbol("test"); - expect(unpack(expression)).toBe(expression); + it("should not export an unpack function, so that the default (identity function) is used", () => { + expect(namespace.unpack).toBe(undefined); // eslint-disable-line import/namespace }); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js index 64891af..09c0a90 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js @@ -7,8 +7,10 @@ const generatePointCut = ({ pointCuts, joinPointPath }) => { } = pointCuts.find(({ name }) => name === pointCutName); return `import togglePoint from "${togglePointModuleSpecifier}"; import handlerFactory from "${toggleHandlerFactoryModuleSpecifier}"; -import { pack, unpack } from "${adapterModuleSpecifier}"; -const handler = handlerFactory({ togglePoint, pack, unpack }); +import * as namespace from "${adapterModuleSpecifier}"; +const identity = (module) => module; +const { pack:_pack = identity, unpack:_unpack = identity } = namespace; +const handler = handlerFactory({ togglePoint, pack: _pack, unpack: _unpack }); export default handler;`; }; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js index da9a5a4..529cb0b 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js @@ -39,15 +39,18 @@ describe("generatePointCut", () => { ); }); - it("should return a script that imports the pack and unpack exports of the appropriate adapter module", () => { + it("should return a script that imports the pack and unpack exports of the appropriate adapter module, via the namespace, falling back to an identity function if the adapter does not export a pack/unpack handler", () => { + // N.B. The pack and unpack functions must be aliased during the import to mitigate https://github.com/webpack/webpack/issues/19518 expect(result).toMatch( - `import { pack, unpack } from "${adapterModuleSpecifier}";` + `import * as namespace from "${adapterModuleSpecifier}"; +const identity = (module) => module; +const { pack:_pack = identity, unpack:_unpack = identity } = namespace;` ); }); it("should return a script that constructs a toggle handler, passing the toggle point to the factory, plus the pack and unpack functions from the load strategy adapter", () => { expect(result).toMatch( - "const handler = handlerFactory({ togglePoint, pack, unpack });" + "const handler = handlerFactory({ togglePoint, pack: _pack, unpack: _unpack });" ); }); From 2d71b5ab53c0e32000a5ae653edabbdf588bee50 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 12 May 2025 21:34:16 +0100 Subject: [PATCH 22/89] remove needless guard revealed by coverage report --- .../processVariantFiles/isJoinPointInvalid.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/isJoinPointInvalid.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/isJoinPointInvalid.js index 90c2b75..1b4352f 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/isJoinPointInvalid.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/isJoinPointInvalid.js @@ -41,10 +41,8 @@ const isJoinPointInvalid = async ({ }) => { await ensureConfigFile({ configFiles, fileSystem, joinDirectory, appRoot }); - if (configFiles.has(joinDirectory)) { - if (configFiles.get(joinDirectory)?.joinPoints.includes(name) === false) { - return true; - } + if (configFiles.get(joinDirectory)?.joinPoints.includes(name) === false) { + return true; } if (!(await fileExists(fileSystem, join(appRoot, joinPointPath)))) { From 63e38a02e69c5b0a29b06d0e77894ffcfa49fc27 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 15 May 2025 21:19:58 +0100 Subject: [PATCH 23/89] react-pointcuts updates for load strategies --- packages/react-pointcuts/babel.jest.json | 12 ++ packages/react-pointcuts/build/rollup.mjs | 22 +-- packages/react-pointcuts/docs/CHANGELOG.md | 22 +++ packages/react-pointcuts/docs/README.md | 36 ++++- packages/react-pointcuts/jest.config.js | 4 - packages/react-pointcuts/jest.config.json | 7 + packages/react-pointcuts/package.json | 17 ++- .../src/lazyComponentLoadStrategyFactory.js | 11 ++ .../lazyComponentLoadStrategyFactory.test.js | 65 ++++++++ .../src/useCodeMatches/getMatchedVariant.js | 6 +- .../useCodeMatches/getMatchedVariant.test.js | 4 +- .../getComponent/index.js | 51 +++---- .../getComponent/index.test.js | 52 +++++-- .../getComponent/withErrorBoundary.js | 18 ++- .../getComponent/withErrorBoundary.test.js | 144 ++++++++---------- .../getComponent/withPlugins.js | 17 ++- .../getComponent/withPlugins.test.js | 72 ++++----- .../withTogglePointFactory/getDisplayName.js | 5 + .../getDisplayName.test.js | 40 +++++ .../src/withTogglePointFactory/index.js | 61 ++++++-- .../src/withTogglePointFactory/index.test.js | 96 +++++++----- .../useDeferredValueWhereAvailable.js | 10 ++ .../useDeferredValueWhereAvailable.test.js | 37 +++++ .../src/withToggledHookFactory/index.js | 25 ++- .../src/withToggledHookFactory/index.test.js | 26 +++- 25 files changed, 596 insertions(+), 264 deletions(-) create mode 100644 packages/react-pointcuts/babel.jest.json delete mode 100644 packages/react-pointcuts/jest.config.js create mode 100644 packages/react-pointcuts/jest.config.json create mode 100644 packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.js create mode 100644 packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js create mode 100644 packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.js create mode 100644 packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.test.js create mode 100644 packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.js create mode 100644 packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.test.js diff --git a/packages/react-pointcuts/babel.jest.json b/packages/react-pointcuts/babel.jest.json new file mode 100644 index 0000000..984bae4 --- /dev/null +++ b/packages/react-pointcuts/babel.jest.json @@ -0,0 +1,12 @@ +{ + "plugins": [ + [ + "babel-plugin-transform-import-meta-x", + { + "replacements": { + "filename": "__filename" + } + } + ] + ] +} diff --git a/packages/react-pointcuts/build/rollup.mjs b/packages/react-pointcuts/build/rollup.mjs index d893632..3227d06 100644 --- a/packages/react-pointcuts/build/rollup.mjs +++ b/packages/react-pointcuts/build/rollup.mjs @@ -1,4 +1,3 @@ -import pkg from "../package.json" with { type: "json" }; import babel from "@rollup/plugin-babel"; import resolve from "@rollup/plugin-node-resolve"; import external from "rollup-plugin-auto-external"; @@ -8,22 +7,25 @@ import keepExternalComments from "./keepExternalComments.mjs"; export default ({ config_isClient }) => { const CLIENT = JSON.parse(config_isClient); - const [esOutputFile, cjsOutputFile, extraPlugins] = { - false: [pkg.exports.node.import, pkg.exports.node.require, []], - true: [pkg.exports.default.import, pkg.exports.default.require, [terser()]] - }[CLIENT]; - return { - input: "./src/index.js", + input: { + main: "./src/index.js", + lazyComponentLoadStrategyFactory: + "./src/lazyComponentLoadStrategyFactory.js" + }, output: [ { - file: esOutputFile, + dir: "lib/", + exports: "named", format: "es", + entryFileNames: "[name].js", sourcemap: true }, { - file: cjsOutputFile, + dir: "lib/", + exports: "named", format: "cjs", + entryFileNames: "[name].es5.cjs", sourcemap: true } ], @@ -39,7 +41,7 @@ export default ({ config_isClient }) => { }), commonjs(), external(), - ...extraPlugins + ...[CLIENT ? terser() : []] ], preserveSymlinks: true }; diff --git a/packages/react-pointcuts/docs/CHANGELOG.md b/packages/react-pointcuts/docs/CHANGELOG.md index d8cb61a..4a2fc00 100644 --- a/packages/react-pointcuts/docs/CHANGELOG.md +++ b/packages/react-pointcuts/docs/CHANGELOG.md @@ -5,6 +5,28 @@ 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.5.0] - ?? + +### Added + +- support for "packed" modules in the `withToggledPointFactory` and `withToggledHookFactory` + - this allows for load strategies to store variations in a form that prevents early download and/or execution, providing a means to "unpack" the module when it is selected / actually ready to render + - the update to `withToggledHookFactory` allows for "deferred execution" loading strategies, but does not support code-split / lazy loading, since no way to inject suspense boundaries within execution path of hooks +- detection of modules packed with [`React.lazy`](https://react.dev/reference/react/lazy) in the `withTogglePointFactory` + - wraps the lazy loaded components in a [Suspense boundary](https://react.dev/reference/react/Suspense) that preserves server-rendered markup whilst variant bundles are downloading. + - utilises [useDeferredValue](https://react.dev/reference/react/useDeferredValue) where available (React 18+) to ensure that when changing between variants (i.e. dynamic feature stores) the existing variant is preserved whilst new variants download, preventing `null`-rendering flash of no content +- a `lazyComponentLoadStrategyFactory` path export + - this creates load strategies for the webpack plugin to "pack" components in `React.lazy` + - added dependency on the webpack package, to support this + +### Changed + +- updated the interface of `withTogglePoint` to de-structure an object, rather than have multiple parameters, aligning with change to the Webpack Plugin, made to support toggle points that only care about a `featuresMap`, or perhaps aligned to a load strategy that does not need `pack` and/or `unpack`. This also aligns with ASOS Codebase Convention PC14 + +### Fixed + +- support the `variantKey` parameter for `withToggledHookFactory`, as already existed for `withTogglePointFactory` + ## [0.4.3] - 2025-03-03 ### Changed diff --git a/packages/react-pointcuts/docs/README.md b/packages/react-pointcuts/docs/README.md index 92347dc..92a55df 100644 --- a/packages/react-pointcuts/docs/README.md +++ b/packages/react-pointcuts/docs/README.md @@ -1,28 +1,37 @@ # @asos/web-toggle-point-react-pointcuts -This package provides react application pointcut code, acting as a target toggle point injected by [`@asos/web-toggle-point-webpack/plugin`](../../webpack/docs/README.md) +This package provides react application pointcut code, acting as a target toggle point injected by the [`TogglePointInjectionPlugin`](../../webpack/docs/README.md) -It contains the following exports: +It contains the following exports from the base package (`@asos/web-toggle-point-react-pointcuts`): - `withTogglePointFactory` -This is a factory function used to create a `withTogglePoint` [react higher-order component](https://reactjs.org/docs/higher-order-components.html). +This is a factory function used to create a `withTogglePoint` [react higher-order component](https://reactjs.org/docs/higher-order-components.html). - `withToggledHookFactory` This is a factory function used to create a [hook](https://reactjs.org/docs/hooks-intro.html)-wrapping function. -The product of both these factories receive the outcome of the webpack plugin, used to choose appropriate variants at runtime, based on decisions from a supplied context. +The product of both these factories receive the outcome of the webpack plugin, used to choose appropriate variants at runtime, based on decisions from a supplied context. Both accept plugins, currently supporting a hook called during code activation (mounting of the component, or calling the hook). +It also contains a package export (accessed via `@asos/web-toggle-point-react-pointcuts/lazyComponentLoadStrategyFactory`): + +- `lazyComponentLoadStrategyFactory` + +This is a load strategy for use with the webpack [`TogglePointInjectionPlugin`](../../webpack/docs/README.md) for when lazy-loading components is desired. + ## Usage +The package has a peer dependency requirement of [`react`](https://github.com/facebook/react/tree/main/packages/react) (with version-matched [`react-is`](https://github.com/facebook/react/tree/main/packages/react-is)), and should work with React 17 and above. + See: [JSDoc output](https://asos.github.io/web-toggle-point/module-web-toggle-point-react-pointcuts.html) > [!WARNING] > ### Use with React 17 -> The package should work with React 17 and above, but due to [a bug](https://github.com/facebook/react/issues/20235) that they are not back-filling, the use of `"type": "module"` in the package means webpack will be unable to resolve the extensionless import. +> The package should work with React 17 and above, but due to [a bug](https://github.com/facebook/react/issues/20235) that they are not back-filling, the use of `"type": "module"` +> in the package means webpack will be unable to resolve the extensionless import. > To fix, either upgrade to React 18+ or add the following resolve configuration to the webpack config: > ```js > resolve: { @@ -31,3 +40,20 @@ See: [JSDoc output](https://asos.github.io/web-toggle-point/module-web-toggle-po > } > } > ``` +> [!IMPORTANT] +> +> Since React 17 does not support suspense for code splitting during server-side rendering, where a `lazyComponentLoadStrategyFactory` strategy is used, this will preclude the use of Server-Side Rendering. + +> [!WARNING] +> ### Use with NextJS +> The package will work with NextJs (see caveats in [next example](../../../examples/next/README.md)), but since NextJs uses it's own "pre-compiled" versions of +> `react` and `react-is`, the `Symbol` used to mark react types needs to be aligned, since this package uses `react-is` to detect if a toggled component is "lazy" or not. +> To ensure that the next-specific version of `react-is` is used by the application build, it can be aliased in the webpack config of the NextJs app thus: +> ```js +> resolve: { +> alias: { +> "react-is": `next/dist/compiled/react-is/cjs/react-is.${process.env.NODE_ENV === "production" ? "production" : "development"}.js` +> } +>} +> ``` +> N.B. The compiled entrypoint for CJS doesn't re-export named exports properly, so you'll need to select the production or development build as appropriate, as shown. diff --git a/packages/react-pointcuts/jest.config.js b/packages/react-pointcuts/jest.config.js deleted file mode 100644 index 14ba02e..0000000 --- a/packages/react-pointcuts/jest.config.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - testEnvironment: "jsdom", - setupFilesAfterEnv: ["./jest.setup-after-env.js"] -}; diff --git a/packages/react-pointcuts/jest.config.json b/packages/react-pointcuts/jest.config.json new file mode 100644 index 0000000..3200bde --- /dev/null +++ b/packages/react-pointcuts/jest.config.json @@ -0,0 +1,7 @@ +{ + "testEnvironment": "jsdom", + "setupFilesAfterEnv": ["./jest.setup-after-env.js"], + "transform": { + "\\.js$": ["babel-jest", { "configFile": "./babel.jest.json" }] + } +} \ No newline at end of file diff --git a/packages/react-pointcuts/package.json b/packages/react-pointcuts/package.json index f6ed552..f1c696a 100644 --- a/packages/react-pointcuts/package.json +++ b/packages/react-pointcuts/package.json @@ -6,13 +6,13 @@ "type": "module", "main": "./lib/main.es5.cjs", "exports": { - "node": { + ".": { "import": "./lib/main.js", "require": "./lib/main.es5.cjs" }, - "default": { - "import": "./lib/browser.js", - "require": "./lib/browser.es5.cjs" + "./lazyComponentLoadStrategyFactory": { + "import": "./lib/lazyComponentLoadStrategyFactory.js", + "require": "./lib/lazyComponentLoadStrategyFactory.es5.cjs" } }, "repository": { @@ -27,6 +27,8 @@ "doc": "docs" }, "scripts": { + "build-dependencies": "path-exists ../../packages/webpack/lib || npm run --prefix ../../packages/webpack build", + "prebuild": "npm run build-dependencies", "build": "npm run clean && npm run build:browser && npm run build:server", "build:browser": "cross-env BABEL_ENV=browser rollup -c build/rollup.mjs --config_isClient true", "build:server": "rollup -c build/rollup.mjs --config_isClient false", @@ -38,6 +40,7 @@ "test": "jest" }, "dependencies": { + "@asos/web-toggle-point-webpack": "file:../webpack", "@babel/runtime": "^7.26.0" }, "devDependencies": { @@ -49,6 +52,7 @@ "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^16.0.1", "@types/webpack-env": "^1.18.0", + "babel-plugin-transform-import-meta-x": "^0.0.3", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.31.10", @@ -59,15 +63,14 @@ "jest-extended": "^4.0.2", "jsdoc": "^4.0.4", "react": "^18.3.1", - "react-dom": "^18.3.1", + "react-is": "^18.3.1", "rimraf": "^6.0.1", "rollup-plugin-auto-external": "^2.0.0" }, "peerDependencies": { "@asos/web-toggle-point-features": "file:../features", - "@asos/web-toggle-point-webpack": "file:../webpack", "prop-types": "^15.7.2", "react": ">=17", - "react-dom": ">=17" + "react-is": ">=17" } } diff --git a/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.js b/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.js new file mode 100644 index 0000000..9c0822d --- /dev/null +++ b/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.js @@ -0,0 +1,11 @@ +import { lazy } from "react"; +// eslint-disable-next-line import/no-unresolved -- https://github.com/import-js/eslint-plugin-import/issues/1810 +import deferredDynamicImportLoadStrategyFactory from "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory"; + +const adapterModuleSpecifier = import.meta.filename; + +export const pack = (expression) => lazy(expression); +export default (options) => ({ + ...deferredDynamicImportLoadStrategyFactory(options), + adapterModuleSpecifier +}); diff --git a/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js b/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js new file mode 100644 index 0000000..a702f94 --- /dev/null +++ b/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js @@ -0,0 +1,65 @@ +/* eslint-disable import/namespace */ +import lazyComponentLoadStrategyFactory, * as namespace from "./lazyComponentLoadStrategyFactory.js"; +// eslint-disable-next-line import/no-unresolved -- https://github.com/import-js/eslint-plugin-import/issues/1810 +import deferredDynamicImportLoadStrategyFactory from "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory"; +import { lazy } from "react"; + +const mockLazyResult = Symbol("test-lazy-result"); +jest.mock("react", () => ({ + lazy: jest.fn(() => mockLazyResult) +})); + +const mockImportCodeGenerator = Symbol("test-import-code-generator"); +jest.mock( + "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory", + () => + jest.fn(() => ({ + importCodeGenerator: mockImportCodeGenerator + })) +); + +describe("lazyComponentLoadStrategyFactory", () => { + const options = Symbol("test-options"); + let result; + + beforeEach(() => { + result = lazyComponentLoadStrategyFactory(options); + }); + + it("should call the deferredDynamicImportLoadStrategyFactory with the options passed to it", () => { + expect(deferredDynamicImportLoadStrategyFactory).toHaveBeenCalledWith( + options + ); + }); + + it("should return an object containing the adapterModuleSpecifier, indicating the location of the factory code file (so that named exports for the pack and unpack functions can be included in the webpack compilation) and importCodeGenerator", () => { + expect(result).toEqual( + expect.objectContaining({ + adapterModuleSpecifier: expect.stringMatching( + /packages\/react-pointcuts\/src\/lazyComponentLoadStrategyFactory\.js$/ // TODO: make work on windows! + ) + }) + ); + }); + + it("should return the importCodeGenerator from the deferredDynamicImportLoadStrategyFactory", () => { + expect(result).toEqual( + expect.objectContaining({ + importCodeGenerator: mockImportCodeGenerator + }) + ); + }); + + it("should export a pack function that wraps its input in React.lazy", () => { + const expression = Symbol("test-expression"); + const packResult = namespace.pack(expression); + expect(lazy).toHaveBeenCalledWith(expression); + expect(packResult).toBe(mockLazyResult); + }); + + describe("unpack", () => { + it("should not export an unpack function, so that the default (identity function) is used", () => { + expect(namespace.unpack).toBe(undefined); + }); + }); +}); diff --git a/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.js b/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.js index 7f3fb15..bda5115 100644 --- a/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.js +++ b/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.js @@ -3,10 +3,10 @@ const getMatchedVariant = ({ matchedFeatures, featuresMap, variantKey }) => { feature, { [variantKey]: variant, ...variables } ] of matchedFeatures) { - const codeRequest = featuresMap.get(feature)?.get(variant); - if (codeRequest) { + const packedModule = featuresMap.get(feature)?.get(variant); + if (packedModule) { return { - codeRequest, + packedModule, variables }; } diff --git a/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.test.js b/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.test.js index d632b15..7b8301b 100644 --- a/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.test.js +++ b/packages/react-pointcuts/src/useCodeMatches/getMatchedVariant.test.js @@ -32,11 +32,11 @@ describe("getMatchedVariant", () => { [feature3, { bucket: bucket1 }] ]; - it("should return the first matching variant where a folder and file match exists in the features map, and return the code request, and the associated variables from the matched features", () => { + it("should return the first matching variant where a folder and file match exists in the features map, and return the packed module, and the associated variables from the matched features", () => { expect( getMatchedVariant({ matchedFeatures, featuresMap, variantKey }) ).toEqual({ - codeRequest: featuresMap.get(feature1).get(bucket2), + packedModule: featuresMap.get(feature1).get(bucket2), variables }); }); diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.js b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.js index 592d6d9..0117487 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.js @@ -1,52 +1,49 @@ import withPlugins from "./withPlugins"; import withErrorBoundary from "./withErrorBoundary"; import { forwardRef } from "react"; +import getDisplayName from "../getDisplayName"; const getControlOrVariant = ({ matchedFeatures, matchedVariant, logError, - control + packedBaseModule, + unpackComponent }) => { - if (!matchedFeatures.length) { - return control; + if (!matchedFeatures.length || !matchedVariant) { + return unpackComponent(packedBaseModule); } - let Component = control; - if (matchedVariant) { - const { codeRequest, variables } = matchedVariant; - const { default: VariantWithoutVariables } = codeRequest; - const Variant = forwardRef((props, ref) => ( - - )); - Variant.displayName = `Variant(${ - VariantWithoutVariables.displayName || - VariantWithoutVariables.name || - "Component" - })`; - - Component = withErrorBoundary({ - Variant, - logError, - fallback: control - }); - } - return Component; + const { packedModule, variables } = matchedVariant; + const VariantWithoutVariables = unpackComponent(packedModule); + const Variant = forwardRef((props, ref) => ( + + )); + Variant.displayName = `Variant(${getDisplayName(VariantWithoutVariables)})`; + + const component = withErrorBoundary({ + Variant, + logError, + packedBaseModule, + unpackComponent + }); + + return component; }; const getComponent = (params) => { - let Component = getControlOrVariant(params); + let component = getControlOrVariant(params); const { plugins, ...rest } = params; if (plugins) { - Component = withPlugins({ - Component, + component = withPlugins({ + component, plugins, ...rest }); } - return Component; + return component; }; export default getComponent; diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.test.js b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.test.js index 401ad62..2e3c5d3 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.test.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/index.test.js @@ -3,15 +3,24 @@ import withErrorBoundary from "./withErrorBoundary"; import withPlugins from "./withPlugins"; import { render, screen } from "@testing-library/react"; import { createRef, forwardRef } from "react"; +import getDisplayName from "../getDisplayName"; jest.mock("./withErrorBoundary", () => jest.fn()); jest.mock("./withPlugins", () => jest.fn()); +const mockDisplayName = "test-display-name"; +jest.mock("../getDisplayName", () => jest.fn(() => mockDisplayName)); const mockVariantComponent = "test-variant-component"; const MockVariantComponent = forwardRef( jest.fn((_, ref) =>

) ); +const unpackMarker = Symbol("test-unpack-marker"); +const unpackComponent = jest.fn().mockImplementation((module) => { + module[unpackMarker] = true; + return module; +}); + describe("getComponent", () => { let result, params; const pluginMarker = Symbol("test-plugin-marker"); @@ -20,12 +29,13 @@ describe("getComponent", () => { jest.clearAllMocks(); params = { logError: Symbol("test-error-logger"), - control: () => Symbol("test-base-component"), - plugins: Symbol("test-plugins") + packedBaseModule: () => Symbol("test-base-component"), + plugins: Symbol("test-plugins"), + unpackComponent }; - withPlugins.mockImplementation(({ Component }) => { - Component[pluginMarker] = true; - return Component; + withPlugins.mockImplementation(({ component }) => { + component[pluginMarker] = true; + return component; }); }); @@ -33,7 +43,7 @@ describe("getComponent", () => { it("should run plugins on the the matched features, passing the params that were passed to the component, in-case the plugins need them", () => { const { plugins, ...rest } = params; expect(withPlugins).toHaveBeenCalledWith({ - Component: result, + component: result, plugins, ...rest }); @@ -42,8 +52,10 @@ describe("getComponent", () => { }; const makeFallbackAssertion = () => { - it("should return the control (base) component", () => { - expect(result).toBe(params.control); + it("should return the unpacked base component", () => { + expect(unpackComponent).toHaveBeenCalledWith(params.packedBaseModule); + expect(result).toBe(params.packedBaseModule); + expect(result[unpackMarker]).toBe(true); }); }; @@ -100,7 +112,7 @@ describe("getComponent", () => { params = { ...params, matchedVariant: { - codeRequest: { default: MockVariantComponent }, + packedModule: MockVariantComponent, variables } }; @@ -108,18 +120,25 @@ describe("getComponent", () => { }); it("should wrap the variant with an error boundary, to ensure errors in the variant result in falling back to the base/default component", () => { - const { control: Component } = params; + const { packedBaseModule: Component } = params; expect(withErrorBoundary).toHaveBeenCalledWith({ logError: params.logError, Variant: expect.anything(), - fallback: Component + packedBaseModule: Component, + unpackComponent }); expect(result[errorBoundariedMarker]).toBe(true); }); + it("should set a display name on the variant", () => { + const [[{ Variant }]] = withErrorBoundary.mock.calls; + expect(getDisplayName).toHaveBeenCalledWith(MockVariantComponent); + expect(Variant.displayName).toEqual(`Variant(${mockDisplayName})`); + }); + makeCommonAssertions(); - describe("when the returned variant is rendered", () => { + describe("when the returned component is rendered", () => { const componentProps = { "test-component-prop": Symbol("test-component-prop-value"), [innateProp]: Symbol("test-innate-prop-value") @@ -131,7 +150,14 @@ describe("getComponent", () => { render(); }); - it("should return the variant, sending down any innate props, attaching the passed ref, and any variables from the feature as props (preferring innate props over variables - so features can't overwrite innate/in-built props)", () => { + it("should return the unpacked variant component", () => { + expect(unpackComponent).toHaveBeenCalledWith( + params.matchedVariant.packedModule + ); + expect(MockVariantComponent[unpackMarker]).toBe(true); + }); + + it("should render the variant, sending down any innate props, attaching the passed ref, and any variables from the feature as props (preferring innate props over variables - so features can't overwrite innate/in-built props)", () => { const variantElement = screen.getByTestId(mockVariantComponent); expect(variantElement).toBeInTheDocument(); expect(variantElement).toBe(mockRef.current); diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.js b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.js index 35a60af..71c6584 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.js @@ -1,8 +1,14 @@ import { Component, forwardRef, createContext } from "react"; +import getDisplayName from "../getDisplayName"; const ForwardedRefContext = createContext(); -const withErrorBoundary = ({ Variant, fallback, logError }) => { +const withErrorBoundary = ({ + Variant, + packedBaseModule, + logError, + unpackComponent +}) => { class TogglePointErrorBoundary extends Component { constructor(props) { super(props); @@ -21,7 +27,9 @@ const withErrorBoundary = ({ Variant, fallback, logError }) => { static contextType = ForwardedRefContext; render() { - const Component = this.state.hasError ? fallback : Variant; + const Component = this.state.hasError + ? unpackComponent(packedBaseModule) + : Variant; return ; } @@ -32,9 +40,9 @@ const withErrorBoundary = ({ Variant, fallback, logError }) => { )); - TogglePointErrorBoundaryWithRef.displayName = `withErrorBoundary(${ - Variant.displayName || Variant.name || "Component" - })`; + TogglePointErrorBoundaryWithRef.displayName = `withErrorBoundary(${getDisplayName( + Variant + )})`; return TogglePointErrorBoundaryWithRef; }; diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.test.js b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.test.js index f1ffe6b..36deb35 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.test.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withErrorBoundary.test.js @@ -2,14 +2,18 @@ import withErrorBoundary from "./withErrorBoundary"; import { render, screen } from "@testing-library/react"; import { createRef, forwardRef } from "react"; +import getDisplayName from "../getDisplayName"; const mockLogError = jest.fn(); +const mockDisplayName = "test-display-name"; +jest.mock("../getDisplayName", () => jest.fn(() => mockDisplayName)); + describe("withErrorBoundary", () => { const inboundProps = { "test-prop": Symbol("test-value") }; - const mockFallback = "test-mock-fallback"; - const MockFallback = forwardRef( - jest.fn((_, ref) =>
) + const mockBaseModule = "test-mock-base-module"; + const MockBaseModule = forwardRef( + jest.fn((_, ref) =>
) ); const mockVariant = "test-mock-variant"; const MockVariant = forwardRef( @@ -17,6 +21,7 @@ describe("withErrorBoundary", () => { ); const mockErrorMessage = "test-error"; const mockRef = createRef(); + const unpackComponent = jest.fn((module) => module); let mockError; const MockErrorVariant = () => { throw mockError; @@ -28,104 +33,87 @@ describe("withErrorBoundary", () => { }); let Boundaried; - const makeCommonAssertions = (MockVariant) => { - describe("when rendering the variant and no error occurs", () => { - beforeEach(() => { - Boundaried = withErrorBoundary({ - Variant: MockVariant, - fallback: MockFallback, - logError: mockLogError - }); - render(); - }); - it("should render the variant, passing the inbound props, and not render the fallback", () => { - const variantElement = screen.getByTestId(mockVariant); - expect(variantElement).toBeInTheDocument(); - expect(variantElement).toBe(mockRef.current); - expect(MockVariant.render).toHaveBeenCalledWith(inboundProps, mockRef); - expect(screen.queryByTestId(mockFallback)).not.toBeInTheDocument(); - }); + const makeCommonAssertions = () => { + it("should have a display name that wraps the component in the error boundary", () => { + expect(Boundaried.displayName).toBe( + `withErrorBoundary(${mockDisplayName})` + ); + }); + }; - it("should not log anything", () => { - expect(mockLogError).not.toHaveBeenCalled(); + describe("when rendering the variant and no error occurs", () => { + beforeEach(() => { + Boundaried = withErrorBoundary({ + Variant: MockVariant, + packedBaseModule: MockBaseModule, + logError: mockLogError, + unpackComponent }); + render(); }); - describe("when rendering the variant and an error occurs", () => { - beforeEach(() => { - jest.spyOn(console, "error").mockImplementation(() => {}); - const Boundaried = withErrorBoundary({ - Variant: MockErrorVariant, - fallback: MockFallback, - logError: mockLogError - }); - render(); - }); + makeCommonAssertions(); - afterEach(() => { - console.error.mockRestore(); - }); + it("should get the display name of the variant passed", () => { + expect(getDisplayName).toHaveBeenCalledWith(MockVariant); + }); - it("should render the fallback, passing the inbound props, and no longer render the variant", () => { - const fallbackElement = screen.getByTestId(mockFallback); - expect(fallbackElement).toBeInTheDocument(); - expect(fallbackElement).toBe(mockRef.current); - expect(MockFallback.render).toHaveBeenCalledWith(inboundProps, mockRef); - expect(screen.queryByTestId(mockVariant)).not.toBeInTheDocument(); - }); + it("should render the variant, passing the inbound props, and not render the fallback", () => { + const variantElement = screen.getByTestId(mockVariant); + expect(variantElement).toBeInTheDocument(); + expect(variantElement).toBe(mockRef.current); + expect(MockVariant.render).toHaveBeenCalledWith(inboundProps, mockRef); + expect(screen.queryByTestId(mockBaseModule)).not.toBeInTheDocument(); + }); - it("should log an error indicating that the fallback has been rendered", () => { - expect(mockLogError).toHaveBeenCalledWith(mockError); - }); + it("should not log anything", () => { + expect(mockLogError).not.toHaveBeenCalled(); + }); + }); - it("should update the message on the error to include that the variant has errored", () => { - expect(mockError.message).toBe( - `Variant errored, rendering fallback: ${mockErrorMessage}` - ); + describe("when rendering the variant and an error occurs", () => { + beforeEach(() => { + jest.spyOn(console, "error").mockImplementation(() => {}); + const Boundaried = withErrorBoundary({ + Variant: MockErrorVariant, + packedBaseModule: MockBaseModule, + logError: mockLogError, + unpackComponent }); + render(); }); - }; - describe("when the given component has a displayName", () => { - beforeEach(() => { - MockVariant.displayName = "test-display-name"; + afterEach(() => { + console.error.mockRestore(); }); - makeCommonAssertions(MockVariant); + makeCommonAssertions(); - it("should name the HOC with the fallback component's display name wrapped in 'withErrorBoundary'", () => { - expect(Boundaried.displayName).toBe( - `withErrorBoundary(${MockVariant.displayName})` - ); + it("should get the display name of the variant passed", () => { + expect(getDisplayName).toHaveBeenCalledWith(MockErrorVariant); }); - }); - describe("when the given component does not have a displayName but is a named function", () => { - beforeEach(() => { - delete MockVariant.displayName; - MockVariant.name = "test-name"; + it("should unpack the fallback component", () => { + expect(unpackComponent).toHaveBeenCalledWith(MockBaseModule); }); - makeCommonAssertions(MockVariant); - - it("should name the HOC with the fallback component's name wrapped in 'withErrorBoundary'", () => { - expect(Boundaried.displayName).toBe( - `withErrorBoundary(${MockVariant.name})` - ); + it("should render the fallback, passing the inbound props, and no longer render the variant", () => { + const fallbackElement = screen.getByTestId(mockBaseModule); + expect(fallbackElement).toBeInTheDocument(); + expect(fallbackElement).toBe(mockRef.current); + expect(MockBaseModule.render).toHaveBeenCalledWith(inboundProps, mockRef); + expect(screen.queryByTestId(mockVariant)).not.toBeInTheDocument(); }); - }); - describe("when the given component does not have a displayName or a function name", () => { - beforeEach(() => { - delete MockVariant.displayName; - delete MockVariant.name; + it("should log an error indicating that the fallback has been rendered", () => { + expect(mockLogError).toHaveBeenCalledWith(mockError); }); - makeCommonAssertions(MockVariant); - - it("should name the HOC with 'Component' wrapped in 'withErrorBoundary'", () => { - expect(Boundaried.displayName).toBe("withErrorBoundary(Component)"); + it("should update the message on the error to include that the variant has errored", () => { + expect(mockError.message).toBe( + `Variant errored, rendering fallback: ${mockErrorMessage}` + ); }); }); }); diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.js b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.js index 9d951d8..9a64c24 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.js @@ -1,24 +1,25 @@ import { forwardRef } from "react"; +import getDisplayName from "../getDisplayName"; -const wrap = ({ Component, useHook, name }, rest) => { +const wrap = ({ component: Component, useHook, name }, rest) => { const WithTogglePointPlugin = forwardRef((props, ref) => { useHook(rest); return ; }); - WithTogglePointPlugin.displayName = `With${name}(${ - Component.displayName || Component.name || "Component" - })`; + WithTogglePointPlugin.displayName = `With${name}(${getDisplayName( + Component + )})`; return WithTogglePointPlugin; }; -const withPlugins = ({ Component, plugins, ...rest }) => { +const withPlugins = ({ component, plugins, ...rest }) => { for (const { onCodeSelected: useHook, name } of plugins) { - Component = wrap( + component = wrap( { - Component, + component, useHook, name }, @@ -26,7 +27,7 @@ const withPlugins = ({ Component, plugins, ...rest }) => { ); } - return Component; + return component; }; export default withPlugins; diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.test.js b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.test.js index 5238386..8bf61ce 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.test.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/getComponent/withPlugins.test.js @@ -1,6 +1,9 @@ import { render } from "@testing-library/react"; import withPlugins from "./withPlugins"; import { forwardRef } from "react"; +import getDisplayName from "../getDisplayName"; + +jest.mock("../getDisplayName", () => jest.fn(({ displayName }) => displayName)); describe("withPlugins", () => { const plugins = [ @@ -12,53 +15,42 @@ describe("withPlugins", () => { anotherTestProp: Symbol("test-another-value") }; const mockComponent = "test-component"; + const TestComponent = forwardRef(() => (
)); + TestComponent.displayName = "test-display-name"; - const makeCommonAssertions = () => { - it("should execute the 'onCodeSelected' hooks of all plugins, in reverse order (since first plugin applies closest to the wrapped component), passing the props passed to withPlugins", () => { - let lastPlugin; - plugins.forEach((plugin) => { - expect(plugin.onCodeSelected).toHaveBeenCalledTimes(1); - expect(plugin.onCodeSelected).toHaveBeenCalledWith(rest); - if (lastPlugin) { - // eslint-disable-next-line jest/no-conditional-expect - expect(plugin.onCodeSelected).toHaveBeenCalledBefore( - lastPlugin.onCodeSelected - ); - } - lastPlugin = plugin; - }); - }); - }; + let Wrapped; - describe.each` - displayName | name | expectedDisplayName - ${"TestComponentDisplayName"} | ${"TestComponentName"} | ${"TestComponentDisplayName"} - ${null} | ${"TestComponentName"} | ${"TestComponentName"} - ${null} | ${null} | ${"Component"} - `( - "when the component has a name of $name and a display name of $displayName", - ({ displayName, name, expectedDisplayName }) => { - let Wrapped; + beforeEach(() => { + jest.clearAllMocks(); + Wrapped = withPlugins({ component: TestComponent, plugins, ...rest }); + render(); + }); - beforeEach(() => { - jest.clearAllMocks(); - TestComponent.displayName = displayName; - TestComponent.name = name; + it("should get the display name of the wrapped component", () => { + expect(getDisplayName).toHaveBeenCalledWith(TestComponent); + }); - Wrapped = withPlugins({ Component: TestComponent, plugins, ...rest }); - render(); - }); + it("should have a display name that wraps the component in the plugins, in order", () => { + expect(Wrapped.displayName).toBe( + `With${plugins[1].name}(With${plugins[0].name}(${TestComponent.displayName}))` + ); + }); - it("should have a display name that wraps the component in the plugins, in order", () => { - expect(Wrapped.displayName).toBe( - `With${plugins[1].name}(With${plugins[0].name}(${expectedDisplayName}))` + it("should execute the 'onCodeSelected' hooks of all plugins, in reverse order (since first plugin applies closest to the wrapped component), passing the props passed to withPlugins", () => { + let lastPlugin; + plugins.forEach((plugin) => { + expect(plugin.onCodeSelected).toHaveBeenCalledTimes(1); + expect(plugin.onCodeSelected).toHaveBeenCalledWith(rest); + if (lastPlugin) { + // eslint-disable-next-line jest/no-conditional-expect + expect(plugin.onCodeSelected).toHaveBeenCalledBefore( + lastPlugin.onCodeSelected ); - }); - - makeCommonAssertions(); - } - ); + } + lastPlugin = plugin; + }); + }); }); diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.js b/packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.js new file mode 100644 index 0000000..10c29dc --- /dev/null +++ b/packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.js @@ -0,0 +1,5 @@ +const getDisplayName = (WrappedComponent) => { + return WrappedComponent.displayName || WrappedComponent.name || "Component"; +}; + +export default getDisplayName; diff --git a/packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.test.js b/packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.test.js new file mode 100644 index 0000000..ba244ba --- /dev/null +++ b/packages/react-pointcuts/src/withTogglePointFactory/getDisplayName.test.js @@ -0,0 +1,40 @@ +import getDisplayName from "./getDisplayName"; + +describe("getDisplayName", () => { + let result; + + describe("when the given component has a displayName", () => { + const mockComponent = () => {}; + mockComponent.displayName = "test-component"; + + beforeEach(() => { + result = getDisplayName(mockComponent); + }); + + it("should return the component's display name", () => { + expect(result).toBe(mockComponent.displayName); + }); + }); + + describe("when the given component does not have a displayName, but has a function name", () => { + const mockComponent = () => {}; + + beforeEach(() => { + result = getDisplayName(mockComponent); + }); + + it("should return the function name that constructed the component", () => { + expect(result).toBe("mockComponent"); + }); + }); + + describe("when the given component does not have a displayName or a function name", () => { + beforeEach(() => { + result = getDisplayName(() => {}); + }); + + it("should return 'Component'", () => { + expect(result).toBe("Component"); + }); + }); +}); diff --git a/packages/react-pointcuts/src/withTogglePointFactory/index.js b/packages/react-pointcuts/src/withTogglePointFactory/index.js index ecd92a2..1d15da1 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/index.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/index.js @@ -1,7 +1,25 @@ -import { useMemo, forwardRef } from "react"; +import { useMemo, forwardRef, Suspense, Fragment } from "react"; import getComponent from "./getComponent"; import useCodeMatches from "../useCodeMatches"; import getCodeSelectionPlugins from "../getCodeSelectionPlugins"; +import { useDeferredValue } from "./useDeferredValueWhereAvailable"; +import getDisplayName from "./getDisplayName"; +import { isLazy, isValidElementType } from "react-is"; + +const isModuleLazyComponent = (Module) => + isValidElementType(Module) && isLazy(); + +/** + * @typedef {external.ReactComponentModuleNamespaceObject | function() : external.ReactComponentModuleNamespaceObject | function() : Promise} LoadWrappedReactComponentModule + * @memberof web-toggle-point-react-pointcuts + * @see {@link https://reactjs.org/docs/react-component.html|React.Component} + */ + +/** + * @typedef {external.ReactHookModuleNamespaceObject | function() : external.ReactHookModuleNamespaceObject | function() : Promise} LoadWrappedReactHookModule + * @memberof web-toggle-point-react-pointcuts + * @see {@link https://reactjs.org/docs/hooks-overview.html|React.Hook} + */ // eslint-disable-next-line prettier/prettier, no-empty -- https://github.com/babel/babel/issues/15156 {} @@ -13,7 +31,7 @@ import getCodeSelectionPlugins from "../getCodeSelectionPlugins"; * @param {object} params parameters * @param {function} params.getActiveFeatures a method to get active features. Called honouring the rules of hooks. * @param {external:HostApplication.logError} params.logError a method that logs errors - * @param {string} [params.variantKey=bucket] A key use to identify a variant from the features data structure. Remaining members of the feature will be passed to the variant as props. + * @param {string} [params.variantKey='bucket'] A key use to identify a variant from the features data structure. Remaining members of the feature will be passed to the variant as props. * @param {Array} [params.plugins] plugins to be used when toggling * Will be used when a toggled component throws an error that can be caught by an {@link https://reactjs.org/docs/error-boundaries.html|ErrorBoundary}. * When errors are caught, the control/base code will be used as the fallback component. @@ -35,16 +53,30 @@ const withTogglePointFactory = ({ const codeSelectionPlugins = getCodeSelectionPlugins(plugins); /** - * A React Higher-Order-Component that wraps a base / control component and swaps in a variant when deemed appropriate by a context + * A React Higher-Order-Component that wraps a base / control component and swaps in a variant based on the active features supplied * @function withTogglePoint * @memberof module:web-toggle-point-react-pointcuts - * @param {ReactComponentModuleNamespaceObject} controlModule The control / base module - * @param {external:React.Component} controlModule.default The control react component - * @param {Map} featuresMap A map of features and their variants, with features as top-level keys and variants as nested keys with modules as the values. + * @param {web-toggle-point-react-pointcuts.LoadWrappedReactComponentModule} joinPoint The joinPoint module, packed in a form defined by the loading strategy + * @param {Map>} featuresMap A map of features and their variants, with features as top-level keys and variants as nested keys with react component modules as the values, packed in a form defined by the loading strategy + * @param {function} unpack a function that unpacks the module, returning a react component module * @returns {external:React.Component} Wrapped react component */ - const withTogglePoint = (controlModule, featuresMap) => { - const { default: control } = controlModule; + const withTogglePoint = ({ + joinPoint: packedBaseModule, + featuresMap, + unpack + }) => { + const isLazyComponents = isModuleLazyComponent(packedBaseModule); + const Wrapper = isLazyComponents + ? ({ children }) => ( + {useDeferredValue(children)} + ) + : Fragment; + const unpackComponent = (packedModule) => { + const unpacked = unpack(packedModule); + return isLazyComponents ? unpacked : unpacked.default; + }; + const WithTogglePoint = forwardRef((props, ref) => { const activeFeatures = getActiveFeatures(); const { matchedFeatures, matchedVariant } = useCodeMatches({ @@ -59,18 +91,21 @@ const withTogglePointFactory = ({ matchedFeatures, matchedVariant, logError, - control, + packedBaseModule, + unpackComponent, plugins: codeSelectionPlugins }), [matchedFeatures, matchedVariant] ); - return ; + return ( + + + + ); }); - WithTogglePoint.displayName = `withTogglePoint(${ - control.displayName || control.name || "Component" - })`; + WithTogglePoint.displayName = `withTogglePoint(${getDisplayName(packedBaseModule)})`; return WithTogglePoint; }; diff --git a/packages/react-pointcuts/src/withTogglePointFactory/index.test.js b/packages/react-pointcuts/src/withTogglePointFactory/index.test.js index 462939d..865ceb4 100644 --- a/packages/react-pointcuts/src/withTogglePointFactory/index.test.js +++ b/packages/react-pointcuts/src/withTogglePointFactory/index.test.js @@ -3,7 +3,8 @@ import { render, screen } from "@testing-library/react"; import useCodeMatches from "../useCodeMatches"; import getComponent from "./getComponent"; import getCodeSelectionPlugins from "../getCodeSelectionPlugins"; -import { createRef, forwardRef } from "react"; +import { createRef, forwardRef, lazy } from "react"; +import getDisplayName from "./getDisplayName"; const mockMatches = {}; jest.mock("../useCodeMatches", () => jest.fn(() => mockMatches)); @@ -17,6 +18,8 @@ const MockVariedComponent = forwardRef( ); jest.mock("./getComponent", () => jest.fn(() => MockVariedComponent)); +const mockDisplayName = "test-display-name"; +jest.mock("./getDisplayName", () => jest.fn(() => mockDisplayName)); describe("withTogglePointFactory", () => { let rerender; @@ -26,20 +29,22 @@ describe("withTogglePointFactory", () => { const mockPlugins = [Symbol("test-plugin1"), Symbol("test-plugin2")]; const mockActiveFeatures = Symbol("test-active-features"); const getActiveFeatures = jest.fn(() => mockActiveFeatures); + const mockComponentModule = { default: () =>
test-component
}; - let Toggled, variantKey; + let Toggled, createMockVariant; describe.each` inputVariantKey | expectedVariantKey - ${variantKey} | ${"bucket"} + ${undefined} | ${"bucket"} ${"test-key"} | ${"test-key"} `( "when given a variant key of $inputVariantKey", ({ inputVariantKey, expectedVariantKey }) => { - const makeCommonAssertions = (mockComponent) => { + let unpack; + + const makeCommonAssertions = ({ joinPoint }) => { beforeEach(() => { mockMatches.matchedFeatures = Symbol("test-features"); - mockMatches.matchedVariant = Symbol("test-variant"); jest.clearAllMocks(); const withTogglePoint = withTogglePointFactory({ getActiveFeatures, @@ -47,7 +52,7 @@ describe("withTogglePointFactory", () => { variantKey: inputVariantKey, plugins: mockPlugins }); - Toggled = withTogglePoint({ default: mockComponent }, featuresMap); + Toggled = withTogglePoint({ joinPoint, featuresMap, unpack }); }); it("should get code selection plugins", () => { @@ -55,15 +60,6 @@ describe("withTogglePointFactory", () => { }); const makeRenderedAssertions = () => { - it("should render the varied component, passing the inbound props provided to the HOC", () => { - expect(screen.getByTestId(mockVariedComponent)).toBeInTheDocument(); - const ref = expect.toBeOneOf([null, expect.anything()]); - expect(MockVariedComponent.render).toHaveBeenCalledWith( - inboundProps, - ref - ); - }); - it("should check for code matches, based on the result of the getActiveFeatures method passed and the potential code paths on disk", () => { expect(useCodeMatches).toHaveBeenCalledWith({ featuresMap, @@ -71,6 +67,15 @@ describe("withTogglePointFactory", () => { activeFeatures: mockActiveFeatures }); }); + + it("should render the (potentially) varied component, passing the inbound props provided to the HOC", () => { + expect(screen.getByTestId(mockVariedComponent)).toBeInTheDocument(); + const ref = expect.toBeOneOf([null, expect.anything()]); + expect(MockVariedComponent.render).toHaveBeenCalledWith( + inboundProps, + ref + ); + }); }; const makeGetComponentAssertions = () => { @@ -80,16 +85,35 @@ describe("withTogglePointFactory", () => { matchedFeatures, matchedVariant, logError, - control: mockComponent, + packedBaseModule: joinPoint, + unpackComponent: expect.any(Function), plugins: mockCodeSelectionPlugins }); }); + + describe("When the get component function uses the unpackComponent method passed", () => { + beforeEach(() => { + const { unpackComponent } = getComponent.mock.calls[0][0]; + unpackComponent(mockMatches.matchedVariant); + }); + + it("should unpack the module", () => { + expect(unpack).toHaveBeenCalledWith(mockMatches.matchedVariant); + }); + }); }; const makeCommonAssertions = () => { makeRenderedAssertions(); makeGetComponentAssertions(); + it("should prepare a display name based on the display name of the joinPoint", () => { + expect(getDisplayName).toHaveBeenCalledWith(joinPoint); + expect(Toggled.displayName).toBe( + `withTogglePoint(${mockDisplayName})` + ); + }); + describe("when the component re-renders", () => { beforeEach(() => { getComponent.mockClear(); @@ -102,7 +126,7 @@ describe("withTogglePointFactory", () => { describe("and the matched variant has updated", () => { beforeEach(() => { - mockMatches.matchedVariant = Symbol("test-new-variant"); + mockMatches.matchedVariant = createMockVariant(); rerender(); }); @@ -161,34 +185,28 @@ describe("withTogglePointFactory", () => { }); }; - describe("when the given fallback component has a displayName", () => { - const mockComponent = () => {}; - mockComponent.displayName = "test-component"; - - makeCommonAssertions(mockComponent); - - it("should name the HOC with the fallback component's display name wrapped in 'withTogglePoint'", () => { - expect(Toggled.displayName).toBe( - `withTogglePoint(${mockComponent.displayName})` - ); + describe("when the given joinPoint is a lazy component", () => { + beforeEach(() => { + createMockVariant = () => + lazy(Promise.resolve(Symbol("test-variant"))); + unpack = jest.fn((module) => module); + mockMatches.matchedVariant = createMockVariant(); }); - }); - describe("when the given fallback component does not have a displayName, but has a function name", () => { - const mockComponent = () => {}; - - makeCommonAssertions(mockComponent); - - it("should name the HOC with the fallback component's name wrapped in 'withTogglePoint'", () => { - expect(Toggled.displayName).toBe(`withTogglePoint(mockComponent)`); + makeCommonAssertions({ + joinPoint: lazy(Promise.resolve(mockComponentModule)) }); }); - describe("when the given fallback component does not have a displayName or a function name", () => { - makeCommonAssertions(() => {}); + describe("when the given joinPoint is not a lazy component", () => { + beforeEach(() => { + createMockVariant = () => ({ default: Symbol("test-variant") }); + unpack = jest.fn((module) => module.default); + mockMatches.matchedVariant = createMockVariant(); + }); - it("should name the HOC with 'Component' wrapped in 'withTogglePoint'", () => { - expect(Toggled.displayName).toBe(`withTogglePoint(Component)`); + makeCommonAssertions({ + joinPoint: mockComponentModule }); }); } diff --git a/packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.js b/packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.js new file mode 100644 index 0000000..491c522 --- /dev/null +++ b/packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.js @@ -0,0 +1,10 @@ +let useDeferredValue = (value) => value; + +(async () => { + const React = await import("react"); + if (React.useDeferredValue) { + useDeferredValue = React.useDeferredValue; + } +})(); + +export { useDeferredValue }; diff --git a/packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.test.js b/packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.test.js new file mode 100644 index 0000000..ef680e2 --- /dev/null +++ b/packages/react-pointcuts/src/withTogglePointFactory/useDeferredValueWhereAvailable.test.js @@ -0,0 +1,37 @@ +const mockReact = {}; +jest.isolateModules(() => { + jest.mock("react", () => mockReact); +}); + +describe("useDeferredValueWhereAvailable", () => { + let useDeferredValue; + + beforeEach(async () => { + jest.resetModules(); + }); + + describe("when the feature is not available", () => { + it("should not error, and return an identity function", async () => { + ({ useDeferredValue } = await import("./useDeferredValueWhereAvailable")); + expect(useDeferredValue).toBeInstanceOf(Function); + + const test = Symbol("test-value"); + expect(useDeferredValue(test)).toBe(test); + }); + }); + + describe("when the feature is available", () => { + const mockUseDeferredValue = Symbol("test-useDeferredValue"); + + beforeEach(async () => { + mockReact.useDeferredValue = mockUseDeferredValue; + await Promise.resolve(); + ({ useDeferredValue } = await import("./useDeferredValueWhereAvailable")); // TODO: understand need for double import, why not await Promise.resolve(), process.nextTick() etc. this is a live binding... + ({ useDeferredValue } = await import("./useDeferredValueWhereAvailable")); + }); + + it("should return the feature from React", () => { + expect(useDeferredValue).toBe(mockUseDeferredValue); + }); + }); +}); diff --git a/packages/react-pointcuts/src/withToggledHookFactory/index.js b/packages/react-pointcuts/src/withToggledHookFactory/index.js index 13c7413..5941860 100644 --- a/packages/react-pointcuts/src/withToggledHookFactory/index.js +++ b/packages/react-pointcuts/src/withToggledHookFactory/index.js @@ -11,6 +11,7 @@ import getCodeSelectionPlugins from "../getCodeSelectionPlugins"; * @function * @param {object} params parameters * @param {function} params.getActiveFeatures a method to get active features, which is called honouring the rules of hooks. + * @param {string} [params.variantKey='bucket'] A key use to identify a variant from the features data structure. Remaining members of the feature will be passed to the variant as props. * @param {Array} [params.plugins] plugins to be used when toggling * @returns {module:web-toggle-point-react-pointcuts.withToggledHook} withToggledHook hook function, use to wrap a function (either a hook itself, or a function that must be called wherever a hook can...). * @example @@ -21,30 +22,40 @@ import getCodeSelectionPlugins from "../getCodeSelectionPlugins"; * }); * export default withToggledHook(useMyHook); */ -const withToggledHookFactory = ({ getActiveFeatures, plugins = [] }) => { +const withToggledHookFactory = ({ + getActiveFeatures, + variantKey = "bucket", + plugins = [] +}) => { const codeSelectionPlugins = getCodeSelectionPlugins(plugins); const useCodeSelectionPlugins = pluginsHookFactory(codeSelectionPlugins); /** - * A React hook that wraps a base / control function or hook and swaps in a variant when deemed appropriate by a context + * A React hook that wraps a base / control function or hook and swaps in a variant based on the active features supplied * @function withToggledHook * @memberof module:web-toggle-point-react-pointcuts - * @param {ReactHookModuleNamespaceObject} controlModule The control / base module - * @param {(external:React.Hook|function)} controlModule.default The control react hook or function. - * @param {Map} featuresMap A map of features and their variants, with features as top-level keys and variants as nested keys with modules as the values. + * @param {LoadWrappedReactHookModule} controlModule The control / base module + * @param {Map>} featuresMap A Map of features and their variants, with features as top-level keys and variants as nested keys with loader-wrapped react hook modules as the values * @returns {external:React.Hook} Wrapped function / hook, as a hook (so must be applied in accordance with the {@link https://reactjs.org/docs/hooks-rules.html|rules of hooks}) */ - const withToggledHook = (controlModule, featuresMap) => { + const withToggledHook = ({ + joinPoint: packedBaseModule, + featuresMap, + unpack + }) => { const useTogglePoint = (...args) => { const activeFeatures = getActiveFeatures(); const { matchedVariant } = useCodeMatches({ featuresMap, + variantKey, activeFeatures }); useCodeSelectionPlugins?.(...args); - const { default: hook } = matchedVariant?.codeRequest ?? controlModule; + const { default: hook } = unpack( + matchedVariant?.packedModule ?? packedBaseModule + ); return hook(...args); }; diff --git a/packages/react-pointcuts/src/withToggledHookFactory/index.test.js b/packages/react-pointcuts/src/withToggledHookFactory/index.test.js index 8ad4b9e..23ce620 100644 --- a/packages/react-pointcuts/src/withToggledHookFactory/index.test.js +++ b/packages/react-pointcuts/src/withToggledHookFactory/index.test.js @@ -20,16 +20,23 @@ describe("withToggledHookFactory", () => { const mockPlugins = [Symbol("test-plugin1"), Symbol("test-plugin2")]; const initialProps = Symbol("test-arg"); const mockActiveFeatures = Symbol("test-active-features"); + const variantKey = Symbol("test-variant-key"); const getActiveFeatures = jest.fn(() => mockActiveFeatures); + const unpack = jest.fn((module) => module); beforeEach(() => { jest.clearAllMocks(); jest.mock("../useCodeMatches", () => jest.fn(() => mockMatches)); const withToggledHook = withToggledHookFactory({ getActiveFeatures, + variantKey, plugins: mockPlugins }); - toggledHook = withToggledHook({ default: mockControlHook }, featuresMap); + toggledHook = withToggledHook({ + joinPoint: { default: mockControlHook }, + featuresMap, + unpack + }); }); it("should get code selection plugins", () => { @@ -49,6 +56,7 @@ describe("withToggledHookFactory", () => { it("should get code matches", () => { expect(useCodeMatches).toHaveBeenCalledWith({ featuresMap, + variantKey, activeFeatures: mockActiveFeatures }); }); @@ -62,7 +70,7 @@ describe("withToggledHookFactory", () => { beforeEach(() => { mockMatches.matchedVariant = { - codeRequest: { + packedModule: { default: variant } }; @@ -71,6 +79,12 @@ describe("withToggledHookFactory", () => { })); }); + it("should unpack the variant module", () => { + expect(unpack).toHaveBeenCalledWith( + mockMatches.matchedVariant.packedModule + ); + }); + it("should call and return the output of the matched variant", () => { expect(variant).toHaveBeenCalledWith(initialProps); expect(result.current).toBe(output); @@ -92,6 +106,12 @@ describe("withToggledHookFactory", () => { ({ result } = renderHook(toggledHook, { initialProps })); }); + it("should unpack the control module", () => { + expect(unpack).toHaveBeenCalledWith({ + default: mockControlHook + }); + }); + it("should call and return the output of the fallback (control) hook", () => { expect(mockControlHook).toHaveBeenCalledWith(initialProps); expect(result.current).toBe(output); @@ -106,7 +126,7 @@ describe("withToggledHookFactory", () => { beforeEach(() => { mockMatches.matchedVariant = { - codeRequest: { + packedModule: { default: jest.fn() } }; From d74cfaaa6b63360ca8da21ad8f5bb7fc767452c7 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 15 May 2025 21:25:56 +0100 Subject: [PATCH 24/89] fixup adapterModuleSpecifier assertions to work on Windows filesystems --- .../src/lazyComponentLoadStrategyFactory.test.js | 2 +- .../deferredDynamicImportLoadStrategyFactory.test.js | 5 +++-- .../deferredRequireLoadStrategyFactory.test.js | 5 +++-- .../staticLoadStrategyFactory.test.js | 7 ++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js b/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js index a702f94..b5a162b 100644 --- a/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js +++ b/packages/react-pointcuts/src/lazyComponentLoadStrategyFactory.test.js @@ -36,7 +36,7 @@ describe("lazyComponentLoadStrategyFactory", () => { expect(result).toEqual( expect.objectContaining({ adapterModuleSpecifier: expect.stringMatching( - /packages\/react-pointcuts\/src\/lazyComponentLoadStrategyFactory\.js$/ // TODO: make work on windows! + /packages(\\+|\/)react-pointcuts\1src\1lazyComponentLoadStrategyFactory\.js$/ ) }) ); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js index 6937fa0..1702408 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory.test.js @@ -1,3 +1,4 @@ +/* eslint-disable import/namespace */ import deferredDynamicImportLoadStrategyFactory, * as namespace from "./deferredDynamicImportLoadStrategyFactory.js"; const path = "/test-folder/test-path"; @@ -20,7 +21,7 @@ describe("deferredDynamicImportLoadStrategyFactory", () => { expect(result).toEqual( expect.objectContaining({ adapterModuleSpecifier: expect.stringMatching( - /packages\/webpack\/src\/moduleLoadStrategyFactories\/deferredDynamicImportLoadStrategyFactory\.js$/ + /packages(\\+|\/)webpack\1src\1moduleLoadStrategyFactories\1deferredDynamicImportLoadStrategyFactory\.js$/ ), importCodeGenerator: expect.any(Function) }) @@ -52,7 +53,7 @@ describe("deferredDynamicImportLoadStrategyFactory", () => { describe("pack", () => { it("should not export a pack function, so that the default (identity function) is used", () => { - expect(namespace.pack).toBe(undefined); // eslint-disable-line import/namespace + expect(namespace.pack).toBe(undefined); }); }); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js index a3c6e7e..dfe22a6 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/deferredRequireLoadStrategyFactory.test.js @@ -1,3 +1,4 @@ +/* eslint-disable import/namespace */ import deferredRequireLoadStrategyFactory, * as namespace from "./deferredRequireLoadStrategyFactory.js"; const path = "/test-folder/test-path"; @@ -21,7 +22,7 @@ describe("deferredRequireLoadStrategyFactory", () => { expect(result).toEqual( expect.objectContaining({ adapterModuleSpecifier: expect.stringMatching( - /packages\/webpack\/src\/moduleLoadStrategyFactories\/deferredRequireLoadStrategyFactory\.js$/ + /packages(\\+|\/)webpack\1src\1moduleLoadStrategyFactories\1deferredRequireLoadStrategyFactory\.js$/ ), importCodeGenerator: expect.any(Function) }) @@ -53,7 +54,7 @@ describe("deferredRequireLoadStrategyFactory", () => { describe("pack", () => { it("should not export a pack function, so that the default (identity function) is used", () => { - expect(namespace.pack).toBe(undefined); // eslint-disable-line import/namespace + expect(namespace.pack).toBe(undefined); }); }); diff --git a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js index 05c3f78..b0feef1 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js @@ -1,3 +1,4 @@ +/* eslint-disable import/namespace */ import staticLoadStrategyFactory, * as namespace from "./staticLoadStrategyFactory.js"; const path = "/test-folder/test-path"; @@ -20,7 +21,7 @@ describe("staticLoadStrategyFactory", () => { expect(result).toEqual( expect.objectContaining({ adapterModuleSpecifier: expect.stringMatching( - /packages\/webpack\/src\/moduleLoadStrategyFactories\/staticLoadStrategyFactory\.js$/ + /packages(\\+|\/)webpack\1src\1moduleLoadStrategyFactories\1staticLoadStrategyFactory\.js$/ // TODO: make work on windows! ), importCodeGenerator: expect.any(Function) }) @@ -59,13 +60,13 @@ import * as variant_2 from "${path}${relativePaths[2]}";`); describe("pack", () => { it("should not export a pack function, so that the default (identity function) is used", () => { - expect(namespace.pack).toBe(undefined); // eslint-disable-line import/namespace + expect(namespace.pack).toBe(undefined); }); }); describe("unpack", () => { it("should not export an unpack function, so that the default (identity function) is used", () => { - expect(namespace.unpack).toBe(undefined); // eslint-disable-line import/namespace + expect(namespace.unpack).toBe(undefined); }); }); }); From f74ec9dc6dd27695e5abf1b0208e2752031c4a86 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 15 May 2025 21:41:34 +0100 Subject: [PATCH 25/89] express example --- examples/express/docs/CHANGELOG.md | 10 +++++ examples/express/package.json | 10 +++-- examples/express/src/routes/animals/README.md | 6 ++- .../express/src/routes/animals/togglePoint.js | 2 +- examples/express/src/routes/config/README.md | 9 ++++- examples/express/webpack.config.js | 40 +++++++++++++------ 6 files changed, 58 insertions(+), 19 deletions(-) diff --git a/examples/express/docs/CHANGELOG.md b/examples/express/docs/CHANGELOG.md index 6ac4195..4ec0fbe 100644 --- a/examples/express/docs/CHANGELOG.md +++ b/examples/express/docs/CHANGELOG.md @@ -5,6 +5,16 @@ 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.0] - ?? + +### Changed + +- added `source-map` devtool and `source-map-loader` to add in visualisation of the module structure in browser developer tools +- fixed to exact version of `react` in `dependencies`, and brought in version-linked `react-is`, a new required peer dependency of the `react-pointcuts` package +- updated the `config` example to utilise the `lazyComponentLoadStrategyFactory` from the `react-pointcuts` package +- updated the `animals` example to utilise the `staticLoadStrategyFactory` from the `webpack` package +- updated `webpack` to `5.99.7` + ## [0.2.3] - 2024-12-24 ### Changed diff --git a/examples/express/package.json b/examples/express/package.json index 995407b..f1674c9 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -20,15 +20,17 @@ "lint:docs": "eslint *.md --flag unstable_config_lookup_from_file" }, "dependencies": { - "@asos/web-toggle-point-react-pointcuts": "file:../../packages/react-pointcuts", "@asos/web-toggle-point-features": "file:../../packages/features", + "@asos/web-toggle-point-react-pointcuts": "file:../../packages/react-pointcuts", "@asos/web-toggle-point-ssr": "file:../../packages/ssr", "@asos/web-toggle-point-webpack": "file:../../packages/webpack", "cross-env": "^7.0.3", "express": "^4.17.1", "http-status-codes": "^2.3.0", - "react": ">=17", - "react-dom": ">=17" + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-is": "^18.3.1", + "source-map-loader": "^5.0.0" }, "devDependencies": { "babel-loader": "^9.2.1", @@ -37,7 +39,7 @@ "path-exists-cli": "^2.0.0", "prop-types": "^15.7.2", "style-loader": "^4.0.0", - "webpack": "^5.38.1", + "webpack": "^5.99.7", "webpack-cli": "^4.7.2", "webpack-node-externals": "^3.0.0" }, diff --git a/examples/express/src/routes/animals/README.md b/examples/express/src/routes/animals/README.md index 3d2386d..d750ca2 100644 --- a/examples/express/src/routes/animals/README.md +++ b/examples/express/src/routes/animals/README.md @@ -19,7 +19,11 @@ It also demonstrates the addition of a toggle-specific side-effect, resulting in You should see a picture of a cat or a dog, depending on the version chosen. The contrived application uses a `streamImage` module that accesses a `urlFetcher`, which is varied by the toggle point. -The toggle point is a higher-order function to ensure that each invocation honours the toggle decision, based on current context. As a caveat of toggling, the constructor of the `urlFetcher` must be called on each request, rather than statically enacted at application start-up. The code demonstrates a mitigation of hypothetical cost of re-construction via use of a cache within the toggle point. +## Implementation + +The variant module loading mode is configured as `static`, meaning all variations are added to the module instance cache when the application starts. + +The toggle point is a higher-order function to ensure that each invocation honours the toggle decision, based on current context, forwarding on arguments to a class constructor. As a caveat of `static` toggling, the constructor of the `urlFetcher` must be called on each request, rather than statically enacted at application start-up. The code demonstrates a mitigation of hypothetical cost of re-construction via use of a cache within the toggle point. The toggle point wraps the varied modules in a [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy) to add the logging side-effect. The use of [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) allows the toggle decision to be scoped on a per-request basis, without explicit access to the express request context. The storage is initialised within [middleware](https://expressjs.com/en/resources/middleware.html). diff --git a/examples/express/src/routes/animals/togglePoint.js b/examples/express/src/routes/animals/togglePoint.js index 5a0d73d..03998c0 100644 --- a/examples/express/src/routes/animals/togglePoint.js +++ b/examples/express/src/routes/animals/togglePoint.js @@ -2,7 +2,7 @@ import featuresStore from "./featuresStore.js"; const cache = new WeakMap(); -export default (_, featuresMap) => { +export default ({ featuresMap }) => { return function (...args) { const { default: Choice } = featuresMap.get( `v${featuresStore.getFeatures().version}` diff --git a/examples/express/src/routes/config/README.md b/examples/express/src/routes/config/README.md index 964acf2..98badb2 100644 --- a/examples/express/src/routes/config/README.md +++ b/examples/express/src/routes/config/README.md @@ -8,6 +8,8 @@ It shows how a payload can be included in the props passed to the toggle point c N.B. No implication that this is in any way a _good use_ of "config", it's heavily contrived. +The implementation uses the `lazyComponentLoadStrategyFactory` from the `react-pointcuts` package, to ensure that variant code is bundled independently, and downloaded on demand. + ## Setup 1. `npm install` @@ -15,5 +17,10 @@ N.B. No implication that this is in any way a _good use_ of "config", it's heavi 3. open `localhost:3002/config` in a browser, you should see a medium sized div 4. stop, and re-start the server with `npm run start:small-env` or `npm run start:large-env` 5. open `localhost:3003/config` (small env) or `localhost:3004/config` (large env), and see a different sized (and coloured) `div` shown -6. press the buttons, to demonstrate overriding the initial content serialized on the server. +6. press the buttons, to demonstrate overriding the initial content serialized on the server - N.B. The colourisation is only a result of the stored config, so using the buttons will just change the size + - watch the network tab in developer tools to observe the lazy loading in effect + - try blocking one of the subsequent chunks, you should see the error boundary falling back to the default experience, with: + ``` + ChunkLoadError: Variant errored, rendering fallback: Loading chunk ### failed. + ``` diff --git a/examples/express/webpack.config.js b/examples/express/webpack.config.js index 3507cb4..f45f843 100644 --- a/examples/express/webpack.config.js +++ b/examples/express/webpack.config.js @@ -1,17 +1,21 @@ import { resolve, basename, dirname, posix } from "path"; +import { fileURLToPath } from "url"; import externals from "webpack-node-externals"; -import { TogglePointInjection } from "@asos/web-toggle-point-webpack/plugins"; +import { TogglePointInjectionPlugin } from "@asos/web-toggle-point-webpack/plugins"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; -import { fileURLToPath } from "url"; +import staticLoadStrategyFactory from "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/staticLoadStrategyFactory"; +import lazyComponentLoadStrategyFactory from "@asos/web-toggle-point-react-pointcuts/lazyComponentLoadStrategyFactory"; const configPointCutConfig = { name: "configuration variants", variantGlob: "./src/routes/config/__variants__/*/*/*.jsx", - togglePointModule: "/src/routes/config/togglePoint.js" + togglePointModuleSpecifier: "/src/routes/config/togglePoint.js", + loadStrategy: lazyComponentLoadStrategyFactory() }; const common = { mode: "production", + devtool: "source-map", module: { rules: [ { @@ -26,7 +30,8 @@ const common = { use: [MiniCssExtractPlugin.loader, "css-loader"] } ] - } + }, + plugins: [new MiniCssExtractPlugin()] }; const config = [ @@ -36,14 +41,13 @@ const config = [ output: { path: resolve(dirname(fileURLToPath(import.meta.url)), "bin"), filename: "server.cjs", - clean: true, - chunkFormat: "module" + clean: true }, externals: [externals()], ...common, plugins: [ - new MiniCssExtractPlugin(), - new TogglePointInjection({ + ...common.plugins, + new TogglePointInjectionPlugin({ pointCuts: [ configPointCutConfig, { @@ -55,7 +59,8 @@ const config = [ ...Array(3).fill(".."), basename(variantPath) ), - togglePointModule: "/src/routes/animals/togglePoint.js" + togglePointModuleSpecifier: "/src/routes/animals/togglePoint.js", + loadStrategy: staticLoadStrategyFactory() } ] }) @@ -68,11 +73,22 @@ const config = [ path: resolve(dirname(fileURLToPath(import.meta.url)), "public"), filename: "main.js" }, + ...common, plugins: [ - new MiniCssExtractPlugin(), - new TogglePointInjection({ pointCuts: [configPointCutConfig] }) + ...common.plugins, + new TogglePointInjectionPlugin({ pointCuts: [configPointCutConfig] }) ], - ...common + module: { + ...common.module, + rules: [ + { + test: /\.js$/, + enforce: "pre", + use: ["source-map-loader"] + }, + ...common.module.rules + ] + } } ]; From 8ea8f9a78595b636c5e2fee72b5de8d09d396f7c Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 15 May 2025 21:51:16 +0100 Subject: [PATCH 26/89] serve example update --- examples/serve/README.md | 2 ++ examples/serve/dist/.gitignore | 4 +++- examples/serve/docs/CHANGELOG.md | 13 +++++++++++++ examples/serve/package.json | 2 +- .../serve/src/fixtures/audience/__pointCutConfig.js | 11 +++++++---- .../serve/src/fixtures/audience/__togglePoint.js | 6 +++--- .../serve/src/fixtures/config/__pointCutConfig.js | 7 ++++--- examples/serve/src/fixtures/config/__togglePoint.js | 6 +++--- .../serve/src/fixtures/event/__pointCutConfig.js | 8 ++++---- examples/serve/src/fixtures/event/__togglePoint.js | 6 +++--- .../src/fixtures/translation/__pointCutConfig.js | 6 ++++-- .../serve/src/fixtures/translation/__togglePoint.js | 6 +++--- examples/serve/src/getToggleHandlerPath.js | 6 ------ examples/serve/src/index.js | 2 +- .../listExtractionFromPathSegment.js | 12 ++++++++++++ .../singleFilenameDottedSegment.js | 9 +++++++++ .../src/toggleHandlerFactories/singlePathSegment.js | 9 +++++++++ .../toggleHandlers/listExtractionFromPathSegment.js | 11 ----------- .../toggleHandlers/singleFilenameDottedSegment.js | 8 -------- .../serve/src/toggleHandlers/singlePathSegment.js | 8 -------- examples/serve/webpack.config.js | 12 +++++++++--- 21 files changed, 90 insertions(+), 64 deletions(-) delete mode 100644 examples/serve/src/getToggleHandlerPath.js create mode 100644 examples/serve/src/toggleHandlerFactories/listExtractionFromPathSegment.js create mode 100644 examples/serve/src/toggleHandlerFactories/singleFilenameDottedSegment.js create mode 100644 examples/serve/src/toggleHandlerFactories/singlePathSegment.js delete mode 100644 examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js delete mode 100644 examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js delete mode 100644 examples/serve/src/toggleHandlers/singlePathSegment.js diff --git a/examples/serve/README.md b/examples/serve/README.md index cd95bcf..4310c29 100644 --- a/examples/serve/README.md +++ b/examples/serve/README.md @@ -15,6 +15,7 @@ It demonstrates a setup that utilises the `toggleHandler`, `variantGlob`, and `c - `/src/fixtures/translation/languages/pt-BR/translations.json` (variant) - This uses a `joinPointGlob` setting that points to a single file, rather than attempting to match in sub-directories. - This uses a bespoke toggle handler to match the language to the path. + - This uses the `staticLoadStrategyFactory` from the `webpack` package, meaning all variant modules are imported at application bootstrap. 2. selecting a site-specific method, based on a site prefix of the url. - This uses modules stored at: - `/src/fixtures/config/somethingSiteSpecific.js` (default) @@ -31,6 +32,7 @@ It demonstrates a setup that utilises the `toggleHandler`, `variantGlob`, and `c - `/src/fixtures/audience/cohort-2/bespoke-experience.js` (variant) - This uses a bespoke `controlResolver` which matches an alternate file name for the default to the variants. - This uses a bespoke toggle handler which has no parent folder for variant folders, which are matched using a naming convention in the glob. + - This uses a `deferredDynamicImportLoadStrategyFactory` from the `webpack` package, producing lazy-loaded chunks for variant code. 4. selecting a theme-specific method, based on a date - This uses modules stored at: - `/src/fixtures/event/theme.css` (default) diff --git a/examples/serve/dist/.gitignore b/examples/serve/dist/.gitignore index a9b203a..4a7dfd9 100644 --- a/examples/serve/dist/.gitignore +++ b/examples/serve/dist/.gitignore @@ -1 +1,3 @@ -main.js +*.js +*.map +*.txt \ No newline at end of file diff --git a/examples/serve/docs/CHANGELOG.md b/examples/serve/docs/CHANGELOG.md index 9f2b321..0dca3d3 100644 --- a/examples/serve/docs/CHANGELOG.md +++ b/examples/serve/docs/CHANGELOG.md @@ -5,6 +5,19 @@ 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.0] - ?? + +### Changed + +- moved to `production` webpack mode with `source-map` devtool and `source-map-loader`, for clarity when using dev tools +- updates for new `webpack` package: + - moving to `toggleHandlerFactories` from `toggleHandlers` + - using non-default `loadStrategy` for two of the four examples + - `audience` becomes `deferredImport` (`async`) + - `translation` becomes `static` (which was the previous default...) + - other default to `deferredRequire` (synchronous, but delayed require of module) +- move to use `import.meta.resolve` replacing the hand-rolled `getToggleHandlerPath.js` + ## [0.2.3] - 2025-02-27 ### Changed diff --git a/examples/serve/package.json b/examples/serve/package.json index 20a3f5d..a4dc7c3 100644 --- a/examples/serve/package.json +++ b/examples/serve/package.json @@ -23,7 +23,7 @@ "css-loader": "^7.1.2", "path-exists-cli": "^2.0.0", "style-loader": "^4.0.0", - "webpack": "^5.38.1", + "webpack": "^5.99.7", "webpack-cli": "^4.7.2", "webpack-node-externals": "^3.0.0" }, diff --git a/examples/serve/src/fixtures/audience/__pointCutConfig.js b/examples/serve/src/fixtures/audience/__pointCutConfig.js index 49df1c5..49e2c48 100644 --- a/examples/serve/src/fixtures/audience/__pointCutConfig.js +++ b/examples/serve/src/fixtures/audience/__pointCutConfig.js @@ -1,11 +1,14 @@ import { basename, posix } from "path"; -import getToggleHandlerPath from "../../getToggleHandlerPath.js"; +import loadStrategyFactory from "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/deferredDynamicImportLoadStrategyFactory"; export default { name: "audience", - togglePointModule: "/src/fixtures/audience/__togglePoint.js", + togglePointModuleSpecifier: "/src/fixtures/audience/__togglePoint.js", variantGlob: "./src/fixtures/audience/**/cohort-[1-9]*([0-9])/*.js", - toggleHandler: getToggleHandlerPath("singlePathSegment.js"), + toggleHandlerFactoryModuleSpecifier: import.meta.resolve( + "../../toggleHandlerFactories/singlePathSegment.js" + ), joinPointResolver: (path) => - posix.resolve(path, "../..", basename(path).replace("bespoke", "control")) + posix.resolve(path, "../..", basename(path).replace("bespoke", "control")), + loadStrategy: loadStrategyFactory() }; diff --git a/examples/serve/src/fixtures/audience/__togglePoint.js b/examples/serve/src/fixtures/audience/__togglePoint.js index cce7a81..a40ff32 100644 --- a/examples/serve/src/fixtures/audience/__togglePoint.js +++ b/examples/serve/src/fixtures/audience/__togglePoint.js @@ -1,9 +1,9 @@ import featuresStore from "./__featuresStore.js"; -export default (joinPoint, featuresMap) => { +export default async ({ joinPoint, featuresMap, unpack }) => { const audience = featuresStore.getFeatures(); if (audience && featuresMap.has(audience)) { - return featuresMap.get(audience).default; + return (await unpack(featuresMap.get(audience))).default(); } - return joinPoint.default; + return (await unpack(joinPoint)).default(); }; diff --git a/examples/serve/src/fixtures/config/__pointCutConfig.js b/examples/serve/src/fixtures/config/__pointCutConfig.js index 092ce28..45ffd47 100644 --- a/examples/serve/src/fixtures/config/__pointCutConfig.js +++ b/examples/serve/src/fixtures/config/__pointCutConfig.js @@ -1,10 +1,11 @@ -import getToggleHandlerPath from "../../getToggleHandlerPath.js"; import joinPointResolver from "../../joinPointResolver.js"; export default { name: "configuration", - togglePointModule: "/src/fixtures/config/__togglePoint.js", + togglePointModuleSpecifier: "/src/fixtures/config/__togglePoint.js", variantGlob: "./src/fixtures/config/**/sites/*/*.js", - toggleHandler: getToggleHandlerPath("listExtractionFromPathSegment.js"), + toggleHandlerFactoryModuleSpecifier: import.meta.resolve( + "../../toggleHandlerFactories/listExtractionFromPathSegment.js" + ), joinPointResolver }; diff --git a/examples/serve/src/fixtures/config/__togglePoint.js b/examples/serve/src/fixtures/config/__togglePoint.js index e63afe7..af461be 100644 --- a/examples/serve/src/fixtures/config/__togglePoint.js +++ b/examples/serve/src/fixtures/config/__togglePoint.js @@ -1,9 +1,9 @@ import featuresStore from "./__featuresStore.js"; -export default (joinPoint, featuresMap) => { +export default ({ joinPoint, featuresMap, unpack }) => { const site = featuresStore.getFeatures(); if (featuresMap.has(site)) { - return featuresMap.get(site).default; + return unpack(featuresMap.get(site)).default; } - return joinPoint.default; + return unpack(joinPoint).default; }; diff --git a/examples/serve/src/fixtures/event/__pointCutConfig.js b/examples/serve/src/fixtures/event/__pointCutConfig.js index 857abfc..3cd3021 100644 --- a/examples/serve/src/fixtures/event/__pointCutConfig.js +++ b/examples/serve/src/fixtures/event/__pointCutConfig.js @@ -1,9 +1,9 @@ -import getToggleHandlerPath from "../../getToggleHandlerPath.js"; - export default { name: "event", - togglePointModule: "/src/fixtures/event/__togglePoint.js", + togglePointModuleSpecifier: "/src/fixtures/event/__togglePoint.js", variantGlob: "./src/fixtures/event/**/*.*.css", - toggleHandler: getToggleHandlerPath("singleFilenameDottedSegment.js"), + toggleHandlerFactoryModuleSpecifier: import.meta.resolve( + "../../toggleHandlerFactories/singleFilenameDottedSegment.js" + ), joinPointResolver: (path) => path.replace(/\.([^.]+)\.css$/, ".css") }; diff --git a/examples/serve/src/fixtures/event/__togglePoint.js b/examples/serve/src/fixtures/event/__togglePoint.js index c837238..35a3b66 100644 --- a/examples/serve/src/fixtures/event/__togglePoint.js +++ b/examples/serve/src/fixtures/event/__togglePoint.js @@ -1,9 +1,9 @@ import featuresStore from "./__featuresStore.js"; -export default (joinPoint, featuresMap) => { +export default ({ joinPoint, featuresMap, unpack }) => { const event = featuresStore.getFeatures(); if (featuresMap.has(event)) { - return featuresMap.get(event).default; + return unpack(featuresMap.get(event)).default; } - return joinPoint; + return unpack(joinPoint).default; }; diff --git a/examples/serve/src/fixtures/translation/__pointCutConfig.js b/examples/serve/src/fixtures/translation/__pointCutConfig.js index 3b0e8ee..956885f 100644 --- a/examples/serve/src/fixtures/translation/__pointCutConfig.js +++ b/examples/serve/src/fixtures/translation/__pointCutConfig.js @@ -1,8 +1,10 @@ import joinPointResolver from "../../joinPointResolver.js"; +import loadStrategyFactory from "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/staticLoadStrategyFactory"; export default { name: "translation", - togglePointModule: "/src/fixtures/translation/__togglePoint.js", + togglePointModuleSpecifier: "/src/fixtures/translation/__togglePoint.js", variantGlob: "./src/fixtures/translation/languages/*/*.json", - joinPointResolver + joinPointResolver, + loadStrategy: loadStrategyFactory() }; diff --git a/examples/serve/src/fixtures/translation/__togglePoint.js b/examples/serve/src/fixtures/translation/__togglePoint.js index 5b13eb8..5a2c9ed 100644 --- a/examples/serve/src/fixtures/translation/__togglePoint.js +++ b/examples/serve/src/fixtures/translation/__togglePoint.js @@ -1,9 +1,9 @@ import featuresStore from "./__featuresStore.js"; -export default (joinPoint, featuresMap) => { +export default ({ joinPoint, featuresMap, unpack }) => { const language = featuresStore.getFeatures(); if (featuresMap.has(language)) { - return featuresMap.get(language); + return unpack(featuresMap.get(language)).default; } - return joinPoint; + return unpack(joinPoint).default; }; diff --git a/examples/serve/src/getToggleHandlerPath.js b/examples/serve/src/getToggleHandlerPath.js deleted file mode 100644 index 2c652b0..0000000 --- a/examples/serve/src/getToggleHandlerPath.js +++ /dev/null @@ -1,6 +0,0 @@ -import { posix } from "path"; - -const getToggleHandlerPath = (...paths) => - posix.join("/", "src", "toggleHandlers", ...paths); - -export default getToggleHandlerPath; diff --git a/examples/serve/src/index.js b/examples/serve/src/index.js index c3e4bb1..98976b9 100644 --- a/examples/serve/src/index.js +++ b/examples/serve/src/index.js @@ -14,5 +14,5 @@ import styles from "./fixtures/event/theme.css"; appendText("Some translated content: " + translations.message); appendText("Some site-specific content: " + siteSpecific1()); appendText("Some more site-specific content: " + siteSpecific2()); -appendText("Some audience-specific content: " + audienceSpecific()); +appendText("Some audience-specific content: " + (await audienceSpecific)); appendText("Some event-themed content", styles.theme); diff --git a/examples/serve/src/toggleHandlerFactories/listExtractionFromPathSegment.js b/examples/serve/src/toggleHandlerFactories/listExtractionFromPathSegment.js new file mode 100644 index 0000000..2000c2b --- /dev/null +++ b/examples/serve/src/toggleHandlerFactories/listExtractionFromPathSegment.js @@ -0,0 +1,12 @@ +export default ({ togglePoint, pack, unpack }) => + ({ 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, pack(variantPathMap.get(key))); + } + return map; + }, new Map()); + return togglePoint({ joinPoint: pack(joinPoint), featuresMap, unpack }); + }; diff --git a/examples/serve/src/toggleHandlerFactories/singleFilenameDottedSegment.js b/examples/serve/src/toggleHandlerFactories/singleFilenameDottedSegment.js new file mode 100644 index 0000000..fce79f2 --- /dev/null +++ b/examples/serve/src/toggleHandlerFactories/singleFilenameDottedSegment.js @@ -0,0 +1,9 @@ +export default ({ togglePoint, pack, unpack }) => + ({ joinPoint, variantPathMap }) => { + const featuresMap = new Map(); + for (const key of variantPathMap.keys()) { + const [, , value] = key.split("."); + featuresMap.set(value, pack(variantPathMap.get(key))); + } + return togglePoint({ joinPoint: pack(joinPoint), featuresMap, unpack }); + }; diff --git a/examples/serve/src/toggleHandlerFactories/singlePathSegment.js b/examples/serve/src/toggleHandlerFactories/singlePathSegment.js new file mode 100644 index 0000000..dc5bc80 --- /dev/null +++ b/examples/serve/src/toggleHandlerFactories/singlePathSegment.js @@ -0,0 +1,9 @@ +export default ({ togglePoint, pack, unpack }) => + ({ joinPoint, variantPathMap }) => { + const featuresMap = new Map(); + for (const key of variantPathMap.keys()) { + const [, value] = key.split("/"); + featuresMap.set(value, pack(variantPathMap.get(key))); + } + return togglePoint({ joinPoint: pack(joinPoint), featuresMap, unpack }); + }; diff --git a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js b/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js deleted file mode 100644 index 91b12d7..0000000 --- a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js +++ /dev/null @@ -1,11 +0,0 @@ -export default ({ togglePoint, joinPoint, variants }) => { - const featuresMap = variants.keys().reduce((map, key) => { - const [, , value] = key.split("/"); - const list = value.split(","); - for (const value of list) { - map.set(value, variants.get(key)); - } - return map; - }, new Map()); - return togglePoint(joinPoint, featuresMap); -}; diff --git a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js b/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js deleted file mode 100644 index 7e26117..0000000 --- a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js +++ /dev/null @@ -1,8 +0,0 @@ -export default ({ togglePoint, joinPoint, variants }) => { - const featuresMap = new Map(); - for (const key of variants.keys()) { - const [, , value] = key.split("."); - featuresMap.set(value, variants.get(key)); - } - return togglePoint(joinPoint, featuresMap); -}; diff --git a/examples/serve/src/toggleHandlers/singlePathSegment.js b/examples/serve/src/toggleHandlers/singlePathSegment.js deleted file mode 100644 index aa394e5..0000000 --- a/examples/serve/src/toggleHandlers/singlePathSegment.js +++ /dev/null @@ -1,8 +0,0 @@ -export default ({ togglePoint, joinPoint, variants }) => { - const featuresMap = new Map(); - for (const key of variants.keys()) { - const [, value] = key.split("/"); - featuresMap.set(value, variants.get(key)); - } - return togglePoint(joinPoint, featuresMap); -}; diff --git a/examples/serve/webpack.config.js b/examples/serve/webpack.config.js index ffe3c76..da4fc1c 100644 --- a/examples/serve/webpack.config.js +++ b/examples/serve/webpack.config.js @@ -4,19 +4,20 @@ import audience from "./src/fixtures/audience/__pointCutConfig.js"; import config from "./src/fixtures/config/__pointCutConfig.js"; import translation from "./src/fixtures/translation/__pointCutConfig.js"; import event from "./src/fixtures/event/__pointCutConfig.js"; -import { TogglePointInjection } from "@asos/web-toggle-point-webpack/plugins"; +import { TogglePointInjectionPlugin } from "@asos/web-toggle-point-webpack/plugins"; import { fileURLToPath } from "url"; export default { entry: "./src/index.js", - mode: "development", + mode: "production", + devtool: "source-map", output: { filename: "main.js", path: resolve(dirname(fileURLToPath(import.meta.url)), "dist") }, externals: [externals()], plugins: [ - new TogglePointInjection({ + new TogglePointInjectionPlugin({ pointCuts: [audience, config, translation, event] }) ], @@ -35,6 +36,11 @@ export default { } } ] + }, + { + test: /\.js$/, + enforce: "pre", + use: ["source-map-loader"] } ] } From a6df6c87369ee66a9e1f7fe74926eaa531f3d6eb Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 15 May 2025 21:51:51 +0100 Subject: [PATCH 27/89] remove comment --- .../staticLoadStrategyFactory.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js index b0feef1..7e7779a 100644 --- a/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js +++ b/packages/webpack/src/moduleLoadStrategyFactories/staticLoadStrategyFactory.test.js @@ -21,7 +21,7 @@ describe("staticLoadStrategyFactory", () => { expect(result).toEqual( expect.objectContaining({ adapterModuleSpecifier: expect.stringMatching( - /packages(\\+|\/)webpack\1src\1moduleLoadStrategyFactories\1staticLoadStrategyFactory\.js$/ // TODO: make work on windows! + /packages(\\+|\/)webpack\1src\1moduleLoadStrategyFactories\1staticLoadStrategyFactory\.js$/ ), importCodeGenerator: expect.any(Function) }) From 2be8bab6781c577b5a60d3dddbc152447e76ae07 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 15 May 2025 22:35:43 +0100 Subject: [PATCH 28/89] add "toggled twice" example to "next" --- .../experiments/8-toggled-twice/README.mdx | 37 ++++++++++++++ .../test-variant/__pointCutConfig.js | 17 +++++++ .../test-variant/component.a-m.tsx | 1 + .../test-variant/component.n-z.tsx | 1 + .../test-feature/test-variant/component.tsx | 1 + .../test-feature/test-variant/constants.ts | 2 + .../test-variant/toggleHandlerFactory.ts | 50 +++++++++++++++++++ .../test-variant/withTogglePoint.tsx | 39 +++++++++++++++ .../experiments/8-toggled-twice/component.tsx | 1 + .../experiments/8-toggled-twice/page.tsx | 16 ++++++ .../8-toggled-twice/playwright.spec.ts | 49 ++++++++++++++++++ 11 files changed, 214 insertions(+) create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/README.mdx create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/__pointCutConfig.js create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.a-m.tsx create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.n-z.tsx create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.tsx create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/constants.ts create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/toggleHandlerFactory.ts create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/withTogglePoint.tsx create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/component.tsx create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/page.tsx create mode 100644 examples/next/src/app/fixtures/experiments/8-toggled-twice/playwright.spec.ts diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/README.mdx b/examples/next/src/app/fixtures/experiments/8-toggled-twice/README.mdx new file mode 100644 index 0000000..928c26b --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/README.mdx @@ -0,0 +1,37 @@ +# Toggling Toggles + +## Explanation + +This example demonstrates how different toggle types and conventions can compound. + +The experimental variant is stored in a folder structure: + +``` +./__variants__/test-feature/test-variant/component.tsx +``` + +It has variants that are colocated in the same folder, with a postfix to their filename: + +``` +./__variants__/test-feature/test-variant/component.a-m.tsx +./__variants__/test-feature/test-variant/component.n-z.tsx +``` + +A bespoke toggle handler unpicks the character ranges from the variant filenames, and adds +each letter to the potential variations considered by the join point. + +A bespoke reactive toggle point with coupled features store is set to listen for key presses, +and modify the active feature to match any held alphabetic character, passing a `children` +render prop to the varied component that outputs the key pressed. + +The supplementary join point has a lazy load strategy applied, ensuring that the extra +variations are code-split into their own chunk. Via the use of `importCodeGeneratorOptions`, +a [webpack magic comment](https://webpack.js.org/api/module-methods/#magic-comments) is set, +ensuring that the chunk is preloaded, and named `"toggled-twice-chunk"`. + +## Activation + +If an `experiments` header is set, with a `"test-feature": { "bucket": "test-variant" }` +decision, the experimentally varied component will show. If a key press of an alphabetic +character is held, then this is itself varied, either a "variant 1" or "variant 2" (dependent +on character range), with the key pressed passed as a string child to the component. \ No newline at end of file diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/__pointCutConfig.js b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/__pointCutConfig.js new file mode 100644 index 0000000..0be81aa --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/__pointCutConfig.js @@ -0,0 +1,17 @@ +import lazyComponentLoadStrategyFactory from "@asos/web-toggle-point-react-pointcuts/lazyComponentLoadStrategyFactory"; + +export default { + name: "toggled twice experiment", + togglePointModuleSpecifier: import.meta.resolve("./withTogglePoint.tsx"), + variantGlob: "./src/app/fixtures/experiments/8-toggled-twice/**/*.?-?.tsx", + joinPointResolver: (path) => path.replace(/.-.\.tsx$/, "tsx"), + toggleHandlerFactoryModuleSpecifier: import.meta.resolve( + "./toggleHandlerFactory.ts" + ), + loadStrategy: lazyComponentLoadStrategyFactory({ + importCodeGeneratorOptions: { + webpackMagicComment: + "/* webpackChunkName: 'toggled-twice-chunk', webpackPreload: true */" + } + }) +}; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.a-m.tsx b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.a-m.tsx new file mode 100644 index 0000000..b69bae6 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.a-m.tsx @@ -0,0 +1 @@ +export { default } from "@/components/variant1"; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.n-z.tsx b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.n-z.tsx new file mode 100644 index 0000000..8dbbf42 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.n-z.tsx @@ -0,0 +1 @@ +export { default } from "@/components/variant2"; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.tsx b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.tsx new file mode 100644 index 0000000..b69bae6 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/component.tsx @@ -0,0 +1 @@ +export { default } from "@/components/variant1"; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/constants.ts b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/constants.ts new file mode 100644 index 0000000..08cab23 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/constants.ts @@ -0,0 +1,2 @@ +export const VARIANT_KEY = "key"; +export const FEATURE_KEY = "keypress"; 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 new file mode 100644 index 0000000..1ac7df6 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/toggleHandlerFactory.ts @@ -0,0 +1,50 @@ +import React from "react"; +import { FEATURE_KEY } from "./constants"; + +type ReactComponentModuleType = { default: () => React.JSX.Element }; +type DynamicReactComponentModuleType = () => Promise; +type LazyComponentType = React.LazyExoticComponent<() => React.JSX.Element>; + +interface TogglePointFactory { + togglePoint: ({ + joinPoint, + featuresMap, + unpack + }: { + joinPoint: LazyComponentType; + featuresMap: Map>; + unpack: (value: LazyComponentType) => LazyComponentType; + }) => () => React.JSX.Element; + pack: (value: DynamicReactComponentModuleType) => LazyComponentType; + unpack: (value: LazyComponentType) => LazyComponentType; +} + +interface TogglePoint { + joinPoint: DynamicReactComponentModuleType; + variantPathMap: Map< + string, + () => Promise<{ default: () => React.JSX.Element }> + >; +} + +export default ({ togglePoint, pack, unpack }: TogglePointFactory) => + ({ joinPoint, variantPathMap }: TogglePoint) => { + const variantsMap = new Map(); + const featuresMap = new Map([[FEATURE_KEY, variantsMap]]); + + for (const key of variantPathMap.keys()) { + const packedValue = pack(variantPathMap.get(key)!); + const [, , value] = key.split("."); + const [start, end] = value.split("-"); + + for ( + let charCode = start.charCodeAt(0); + charCode <= end.charCodeAt(0); + charCode++ + ) { + variantsMap.set(String.fromCharCode(charCode), packedValue); + } + } + + return togglePoint({ joinPoint: pack(joinPoint), featuresMap, unpack }); + }; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/withTogglePoint.tsx b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/withTogglePoint.tsx new file mode 100644 index 0000000..eb3a6e7 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/__variants__/test-feature/test-variant/withTogglePoint.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { withTogglePointFactory } from "@asos/web-toggle-point-react-pointcuts"; +import { FEATURE_KEY, VARIANT_KEY } from "./constants"; + +const getActiveFeatures = () => { + const [state, updateState] = useState(null); + const onKeyDown = (event) => { + event.preventDefault(); + updateState({ + [FEATURE_KEY]: { + [VARIANT_KEY]: event.key, + children:

pressed: {event.key}

+ } + }); + }; + const onKeyUp = (event) => { + event.preventDefault(); + updateState(null); + }; + useEffect(() => { + document.addEventListener("keydown", onKeyDown); + document.addEventListener("keyup", onKeyUp); + return () => { + document.removeEventListener("keydown", onKeyDown); + document.removeEventListener("keyup", onKeyUp); + }; + }, [onKeyDown]); + + return state; +}; + +const withTogglePoint = withTogglePointFactory({ + getActiveFeatures, + variantKey: VARIANT_KEY +}); + +export default withTogglePoint; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/component.tsx b/examples/next/src/app/fixtures/experiments/8-toggled-twice/component.tsx new file mode 100644 index 0000000..e5eba80 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/component.tsx @@ -0,0 +1 @@ +export { default } from "@/components/control1"; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/page.tsx b/examples/next/src/app/fixtures/experiments/8-toggled-twice/page.tsx new file mode 100644 index 0000000..143702b --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +import type { JSX } from "react"; +import Component from "./component"; +import ReadMe from "./README.mdx"; + +const Page = (): JSX.Element => ( + <> + +
+ +
+ +); + +export default Page; diff --git a/examples/next/src/app/fixtures/experiments/8-toggled-twice/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/8-toggled-twice/playwright.spec.ts new file mode 100644 index 0000000..9d79ba2 --- /dev/null +++ b/examples/next/src/app/fixtures/experiments/8-toggled-twice/playwright.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from "@playwright/test"; +import setExperimentHeaders from "../playwright.setExperimentHeaders"; +import locateWithinExample from "../playwright.locateInExample"; +import getFixtureURL from "../../playwright.getFixtureUrl"; +const fixtureURL = getFixtureURL(import.meta.url); + +test.describe("varying a component and varying again with a different toggle point, loading mode, file convention", () => { + test.describe("when no experiments header set", () => { + test("it shows the control module", async ({ page }) => { + await page.goto(fixtureURL); + await expect(locateWithinExample(page, "control 1")).toBeVisible(); + }); + }); + + test.describe("when an experiments header set", () => { + setExperimentHeaders(test); + + test("it shows a varied module", async ({ page }) => { + await page.goto(fixtureURL); + await expect(locateWithinExample(page, "variant 1")).toBeVisible(); + }); + + test.describe("when the 'a' key is pressed", () => { + test('it shows "variant 1" and a paragraph with "pressed: a"', async ({ + page + }) => { + await page.goto(fixtureURL); + await page.keyboard.down("a"); + await expect(locateWithinExample(page, "variant 1")).toBeVisible(); + await expect( + page.locator("p", { hasText: "pressed: a" }) + ).toBeVisible(); + }); + }); + + test.describe("when the 'n' key is pressed", () => { + test('it shows "variant 2" and a paragraph with "pressed: n"', async ({ + page + }) => { + await page.goto(fixtureURL); + await page.keyboard.down("n"); + await expect(locateWithinExample(page, "variant 2")).toBeVisible(); + await expect( + page.locator("p", { hasText: "pressed: n" }) + ).toBeVisible(); + }); + }); + }); +}); From da8b418c27b44addde5482e6e597264fc38ff3e3 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 15 May 2025 22:36:36 +0100 Subject: [PATCH 29/89] add "content management" example to "next" --- .../fixtures/content-management/README.mdx | 0 .../content-management/__pointCutConfig.js | 9 ++++ .../devMode/active/useContentEditable.ts | 51 +++++++++++++++++++ .../fixtures/content-management/actions.ts | 17 +++++++ .../contentEditorContext.tsx | 20 ++++++++ .../content-management/featuresStore.ts | 9 ++++ .../fixtures/content-management/layout.tsx | 19 +++++++ .../app/fixtures/content-management/page.tsx | 12 +++++ .../content-management/playwright.spec.ts | 35 +++++++++++++ .../fixtures/content-management/styles.css | 11 ++++ .../content-management/useContentEditable.ts | 3 ++ .../content-management/withToggledHook.ts | 15 ++++++ .../playwright.getFixtureUrl.ts | 0 13 files changed, 201 insertions(+) create mode 100644 examples/next/src/app/fixtures/content-management/README.mdx create mode 100644 examples/next/src/app/fixtures/content-management/__pointCutConfig.js create mode 100644 examples/next/src/app/fixtures/content-management/__variants__/devMode/active/useContentEditable.ts create mode 100644 examples/next/src/app/fixtures/content-management/actions.ts create mode 100644 examples/next/src/app/fixtures/content-management/contentEditorContext.tsx create mode 100644 examples/next/src/app/fixtures/content-management/featuresStore.ts create mode 100644 examples/next/src/app/fixtures/content-management/layout.tsx create mode 100644 examples/next/src/app/fixtures/content-management/page.tsx create mode 100644 examples/next/src/app/fixtures/content-management/playwright.spec.ts create mode 100644 examples/next/src/app/fixtures/content-management/styles.css create mode 100644 examples/next/src/app/fixtures/content-management/useContentEditable.ts create mode 100644 examples/next/src/app/fixtures/content-management/withToggledHook.ts rename examples/next/src/app/fixtures/{experiments => }/playwright.getFixtureUrl.ts (100%) diff --git a/examples/next/src/app/fixtures/content-management/README.mdx b/examples/next/src/app/fixtures/content-management/README.mdx new file mode 100644 index 0000000..e69de29 diff --git a/examples/next/src/app/fixtures/content-management/__pointCutConfig.js b/examples/next/src/app/fixtures/content-management/__pointCutConfig.js new file mode 100644 index 0000000..8e9e0b4 --- /dev/null +++ b/examples/next/src/app/fixtures/content-management/__pointCutConfig.js @@ -0,0 +1,9 @@ +export default [ + { + name: "content management", + togglePointModuleSpecifier: + "/src/app/fixtures/content-management/withToggledHook", + variantGlob: + "./src/app/fixtures/content-management/**/__variants__/*/*/use!(*.spec).ts" + } +]; diff --git a/examples/next/src/app/fixtures/content-management/__variants__/devMode/active/useContentEditable.ts b/examples/next/src/app/fixtures/content-management/__variants__/devMode/active/useContentEditable.ts new file mode 100644 index 0000000..0c0d962 --- /dev/null +++ b/examples/next/src/app/fixtures/content-management/__variants__/devMode/active/useContentEditable.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef, useTransition } from "react"; +import { saveMarkdown } from "../../../actions"; +import type TurndownService from "turndown"; + +const useContentEditable = () => { + const [isPending, startTransition] = useTransition(); + const turndownServiceRef = useRef(undefined); + + useEffect(() => { + if (isPending) { + document.body.setAttribute("data-is-saving", "true"); + } else { + document.body.removeAttribute("data-is-saving"); + } + }, [isPending]); + + const okKeyDown = (e: KeyboardEvent) => { + if (e.metaKey && e.key === "s") { + e.preventDefault(); + startTransition(async () => { + await saveMarkdown( + turndownServiceRef.current?.turndown( + document.body.innerHTML.replaceAll(/