From d0ce7f1f89e770afda63a54d0992b0e33e1ae427 Mon Sep 17 00:00:00 2001 From: Tom Pereira Date: Tue, 24 Dec 2024 17:31:32 +0000 Subject: [PATCH 01/44] 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/44] [#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/44] [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/44] 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 e942cde472bd47b769bfeea78d4b702b4d3a4088 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:01:45 +0100 Subject: [PATCH 05/44] Extract portions of load strategies update and apply separately --- docs/CHANGELOG.md | 7 + eslint.config.mjs | 10 +- examples/express/docs/CHANGELOG.md | 6 + examples/express/package.json | 2 +- .../express/src/routes/animals/middleware.js | 1 - examples/next/docs/CHANGELOG.md | 6 + examples/next/next.config.mjs | 4 +- examples/serve/docs/CHANGELOG.md | 8 +- .../listExtractionFromPathSegment.js | 6 +- .../singleFilenameDottedSegment.js | 6 +- .../src/toggleHandlers/singlePathSegment.js | 6 +- package-lock.json | 53 +--- package.json | 4 +- packages/webpack/docs/CHANGELOG.md | 15 ++ packages/webpack/package.json | 13 +- .../fillDefaultOptionalValues.js | 27 ++ .../fillDefaultOptionalValues.test.js | 84 ++++++ .../src/plugins/togglePointInjection/index.js | 17 +- .../togglePointInjection/index.test.js | 241 +++++++++--------- .../togglePointInjection/integration.test.js | 6 +- .../plugins/togglePointInjection/logger.js | 8 +- .../togglePointInjection/logger.test.js | 13 +- .../fillDefaultOptionalValues.js | 13 - .../fillDefaultOptionalValues.test.js | 74 ------ .../processPointCuts/index.js | 5 +- .../processPointCuts/index.test.js | 20 +- .../processVariantFiles/index.js | 18 +- .../processVariantFiles/index.test.js | 128 +++++----- .../processVariantFiles/linkJoinPoints.js | 13 + .../linkJoinPoints.test.js | 109 ++++++++ .../resolveJoinPoints/index.js | 55 ++-- .../resolveJoinPoints/index.test.js | 21 +- .../setupSchemeModules/generateJoinPoint.js | 20 -- .../generateJoinPoint.test.js | 47 ---- .../generateJoinPoint/createVariantPathMap.js | 5 + .../createVariantPathMap.test.js | 11 + .../generateJoinPoint/importCodeGenerator.js | 15 ++ .../importCodeGenerator.test.js | 44 ++++ .../generateJoinPoint/index.js | 18 ++ .../generateJoinPoint/index.test.js | 51 ++++ .../setupSchemeModules/generatePointCut.js | 4 +- .../generatePointCut.test.js | 6 +- .../setupSchemeModules/index.js | 8 +- .../setupSchemeModules/index.test.js | 21 +- .../pathSegmentToggleHandler.js | 18 +- .../pathSegmentToggleHandler.test.js | 30 ++- packages/webpack/test/test-utils.js | 1 - 47 files changed, 770 insertions(+), 528 deletions(-) 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 delete mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js delete mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.test.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.test.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.js create mode 100644 packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.test.js diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index af73a0d..9ce5f67 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.7] - 2025-07-29 + +### Changed + +- updated to `0.11.0` of [`eslint-plugin-workspaces`](https://github.com/joshuajaco/eslint-plugin-workspaces) after [addition of ESLint9 support](https://github.com/joshuajaco/eslint-plugin-workspaces/commit/af855c3a3d8069366d4446747e91828ddf7560c6) + - update `eslint.config.mjs` to utilise flat config + ## [0.10.6] - 2025-07-14 ### Fixed diff --git a/eslint.config.mjs b/eslint.config.mjs index 93f947b..3ffc16a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,19 +2,13 @@ import asosConfig from "./peripheral/eslint-config-asosconfig/index.js"; import globals from "globals"; import jsdoc from "eslint-plugin-jsdoc"; import markdown from "@eslint/markdown"; -import { FlatCompat } from "@eslint/eslintrc"; -import path from "path"; -import { fileURLToPath } from "url"; +import workspaces from "eslint-plugin-workspaces"; const scripts = ["*.{js,mjs}", "**/*.{js,mjs}"]; const markDowns = ["*.md", "**/*.md"]; -const compat = new FlatCompat({ - baseDirectory: path.dirname(fileURLToPath(import.meta.url)) -}); - export default [ - ...compat.extends("plugin:workspaces/recommended"), + workspaces.configs["flat/recommended"], ...asosConfig.map((config) => ({ files: scripts, ignores: ["**/docs/**", "**/danger/**"], diff --git a/examples/express/docs/CHANGELOG.md b/examples/express/docs/CHANGELOG.md index af7bab2..03a6019 100644 --- a/examples/express/docs/CHANGELOG.md +++ b/examples/express/docs/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.6] - 2025-07-29 + +### Fixed + +- removed "Vary" header from "animals" example, the page is meant to be un-cacheable, and the value was wrong in any case + ## [0.2.5] - 2025-05-27 ### Changed diff --git a/examples/express/package.json b/examples/express/package.json index 07a30e8..4894627 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-express-example", - "version": "0.2.5", + "version": "0.2.6", "type": "module", "engines": { "node": ">=20.6.0" diff --git a/examples/express/src/routes/animals/middleware.js b/examples/express/src/routes/animals/middleware.js index 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 }); }; diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index 91c3ed8..e918e3c 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.5] - 2025-05-29 + +### Changed + +- update to take supply static `webpackNormalModule` corresponding to webpack plugin [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-05-29) + ## [0.2.4] - 2025-05-27 ### Changed diff --git a/examples/next/next.config.mjs b/examples/next/next.config.mjs index 428a031..799b10b 100644 --- a/examples/next/next.config.mjs +++ b/examples/next/next.config.mjs @@ -1,5 +1,6 @@ import createMDX from "@next/mdx"; import remarkGfm from "remark-gfm"; +import webpackNormalModule from "next/dist/compiled/webpack/NormalModule.js"; /** @type {import('next').NextConfig} */ const nextConfig = { @@ -17,8 +18,7 @@ const togglePointInjection = new TogglePointInjection({ ] } ], - webpackNormalModule: async () => - (await import("next/dist/compiled/webpack/NormalModule.js")).default + webpackNormalModule }); nextConfig.webpack = (config) => { diff --git a/examples/serve/docs/CHANGELOG.md b/examples/serve/docs/CHANGELOG.md index b3f8ea1..e2ca06f 100644 --- a/examples/serve/docs/CHANGELOG.md +++ b/examples/serve/docs/CHANGELOG.md @@ -5,11 +5,17 @@ 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.7] - 2025-07-29 + +### Changed + +- updated toggle handlers to take a `variantPathMap` corresponding to webpack [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-05-29) + ## [0.2.6] - 2025-07-14 ### Changed -- updated to use `variantGlobs` array, with updated webpack plugin [0.8.0][version 0.8.0](../../../packages/webpack/docs/CHANGELOG.md#080---2025-05-27) +- updated to use `variantGlobs` array, with updated webpack plugin [version 0.8.0](../../../packages/webpack/docs/CHANGELOG.md#080---2025-05-27) ## [0.2.5] - 2025-07-14 diff --git a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js b/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js index 6fc9234..e8189b7 100644 --- a/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js +++ b/examples/serve/src/toggleHandlers/listExtractionFromPathSegment.js @@ -1,9 +1,9 @@ -export default ({ togglePoint, joinPoint, variants }) => { - const featuresMap = variants.keys().reduce((map, key) => { +export default ({ togglePoint, joinPoint, variantPathMap }) => { + const featuresMap = variantPathMap.keys().reduce((map, key) => { const [, , value] = key.split("/"); const list = value.split(","); for (const value of list) { - map.set(value, variants(key)); + map.set(value, variantPathMap.get(key)); } return map; }, new Map()); diff --git a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js b/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js index d9dc3bc..2d9a6a3 100644 --- a/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js +++ b/examples/serve/src/toggleHandlers/singleFilenameDottedSegment.js @@ -1,8 +1,8 @@ -export default ({ togglePoint, joinPoint, variants }) => { +export default ({ togglePoint, joinPoint, variantPathMap }) => { const featuresMap = new Map(); - for (const key of variants.keys()) { + for (const key of variantPathMap.keys()) { const [, , value] = key.split("."); - featuresMap.set(value, variants(key)); + featuresMap.set(value, variantPathMap.get(key)); } return togglePoint(joinPoint, featuresMap); }; diff --git a/examples/serve/src/toggleHandlers/singlePathSegment.js b/examples/serve/src/toggleHandlers/singlePathSegment.js index 9cf4f2d..7a72925 100644 --- a/examples/serve/src/toggleHandlers/singlePathSegment.js +++ b/examples/serve/src/toggleHandlers/singlePathSegment.js @@ -1,8 +1,8 @@ -export default ({ togglePoint, joinPoint, variants }) => { +export default ({ togglePoint, joinPoint, variantPathMap }) => { const featuresMap = new Map(); - for (const key of variants.keys()) { + for (const key of variantPathMap.keys()) { const [, value] = key.split("/"); - featuresMap.set(value, variants(key)); + featuresMap.set(value, variantPathMap.get(key)); } return togglePoint(joinPoint, featuresMap); }; diff --git a/package-lock.json b/package-lock.json index df4b789..3929385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@asos/web-toggle-point", - "version": "0.10.6", + "version": "0.10.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@asos/web-toggle-point", - "version": "0.10.6", + "version": "0.10.7", "license": "MIT", "workspaces": [ "packages/features", @@ -42,7 +42,7 @@ "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-workspaces": "^0.10.1", + "eslint-plugin-workspaces": "^0.11.0", "globals": "^15.12.0", "jsdoc": "^4.0.4", "lint-staged": "^15.2.10", @@ -57,7 +57,7 @@ }, "examples/express": { "name": "web-toggle-point-express-example", - "version": "0.2.5", + "version": "0.2.6", "dependencies": { "@asos/web-toggle-point-features": "file:../../packages/features", "@asos/web-toggle-point-react-pointcuts": "file:../../packages/react-pointcuts", @@ -6048,7 +6048,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", @@ -8518,11 +8520,13 @@ } }, "node_modules/eslint-plugin-workspaces": { - "version": "0.10.1", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-workspaces/-/eslint-plugin-workspaces-0.11.0.tgz", + "integrity": "sha512-1Ol5QoV+IDBt/YiGCAXWKccKI3AAUSQUmaz0cw0at/MjgEPHvCQAkrv5U2p0C3YInd4sOfBzmyumhWFl6n6INQ==", "dev": true, "license": "MIT", "dependencies": { - "find-workspaces": "^0.3.0" + "find-workspaces": "^0.3.1" } }, "node_modules/eslint-scope": { @@ -16576,30 +16580,6 @@ "@babel/runtime": "^7.8.4" } }, - "node_modules/regexgen": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "jsesc": "^2.3.0", - "regenerate": "^1.3.2" - }, - "bin": { - "regexgen": "bin/cli.js" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/regexgen/node_modules/jsesc": { - "version": "2.5.2", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "license": "MIT", @@ -19936,12 +19916,11 @@ }, "packages/webpack": { "name": "@asos/web-toggle-point-webpack", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "fast-glob": "^3.2.12", - "regexgen": "^1.3.0" + "fast-glob": "^3.2.12" }, "devDependencies": { "@rollup/plugin-babel": "^6.0.2", @@ -19963,13 +19942,7 @@ "webpack-test-utils": "^2.1.0" }, "peerDependencies": { - "next": ">14", "webpack": ">=5.70" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } } }, "peripheral/babel-preset-asos": { diff --git a/package.json b/package.json index c59b22c..1bd3fc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@asos/web-toggle-point", - "version": "0.10.6", + "version": "0.10.7", "repository": "git@github.com:asos/web-toggle-point.git", "homepage": "https://asos.github.io/web-toggle-point/", "license": "MIT", @@ -51,7 +51,7 @@ "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-workspaces": "^0.10.1", + "eslint-plugin-workspaces": "^0.11.0", "globals": "^15.12.0", "jsdoc": "^4.0.4", "lint-staged": "^15.2.10", diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index 8673a3c..66419da 100644 --- a/packages/webpack/docs/CHANGELOG.md +++ b/packages/webpack/docs/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.0] - 2025-07-29 + +### Changed + +- consolidated setting of default optional values +- changed `variants` array on join point data structure to a `Map` of relative to absolute path as `variantPathMap` +- move away from webpack `import.meta.webpackContext` when generating join points, construct a `Map` manually instead + - add linking of join points, to supplant the functionality previously provided by `import.meta.webpackContext` +- updated win32 path replacement, can effectively no-op on posix systems + +### Fixed + +- removed "next" peer dependency, this needn't be explicit +- ensured files that cannot be resolved (by [enhanced-resolve](https://github.com/webpack/enhanced-resolve/)), for whatever reason, don't break the build + ## [0.8.0] - 2025-05-27 ### Changed diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 093b158..02afc72 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -1,7 +1,7 @@ { "name": "@asos/web-toggle-point-webpack", "description": "toggle point webpack plugin", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "type": "module", "main": "./lib/main.cjs", @@ -35,8 +35,7 @@ }, "dependencies": { "@babel/runtime": "^7.26.0", - "fast-glob": "^3.2.12", - "regexgen": "^1.3.0" + "fast-glob": "^3.2.12" }, "devDependencies": { "@rollup/plugin-babel": "^6.0.2", @@ -58,12 +57,6 @@ "webpack-test-utils": "^2.1.0" }, "peerDependencies": { - "webpack": ">=5.70", - "next": ">14" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } + "webpack": ">=5.70" } } diff --git a/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js new file mode 100644 index 0000000..a23fbbe --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.js @@ -0,0 +1,27 @@ +import { posix, basename } from "path"; +import webpack from "webpack"; + +const fillDefaultPointcutValues = (pointCut) => { + const { + variantGlobs = ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], + joinPointResolver = (variantPath) => + posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)), + toggleHandler = "@asos/web-toggle-point-webpack/pathSegmentToggleHandler" + } = pointCut; + return { + ...pointCut, + variantGlobs, + joinPointResolver, + toggleHandler + }; +}; + +const fillDefaultOptionalValues = (options) => { + return { + webpackNormalModule: webpack.NormalModule, + ...options, + pointCuts: options.pointCuts.map(fillDefaultPointcutValues) + }; +}; + +export default fillDefaultOptionalValues; diff --git a/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js new file mode 100644 index 0000000..7e5a2f4 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/fillDefaultOptionalValues.test.js @@ -0,0 +1,84 @@ +import webpack from "webpack"; +import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; + +jest.mock("webpack", () => ({ NormalModule: Symbol("test-normal-module") })); + +describe("fillDefaultOptionalValues", () => { + let result; + + const makeDefaultJoinPointResolverAssertions = () => { + describe("when the joinPointResolver is called", () => { + it("should return a path that is the same as the variantPath, but with the last 3 directories removed", () => { + const variantPath = + "/test-folder/test-sub-folder/test-sub-folder/test-sub-folder/test-variant"; + const joinPointPath = result.joinPointResolver(variantPath); + expect(joinPointPath).toEqual("/test-folder/test-variant"); + }); + }); + }; + + const variantGlobs = Symbol("test-variant-globs"); + const joinPointResolver = Symbol("test-join-point-resolver"); + + const defaultVariantGlobs = [ + "./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}" + ]; + const defaultJoinPointResolver = expect.any(Function); + const defaultToggleHandler = + "@asos/web-toggle-point-webpack/pathSegmentToggleHandler"; + const toggleHandler = Symbol("test-toggle-handler"); + const webpackNormalModule = Symbol("test-webpack-normal-module"); + + describe("when configuring the plugin with a supplied webpackNormalModule", () => { + beforeEach(() => { + result = fillDefaultOptionalValues({ + webpackNormalModule, + pointCuts: [] + }); + }); + + it("should return the supplied webpackNormalModule", () => { + expect(result.webpackNormalModule).toBe(webpackNormalModule); + }); + }); + + describe("when configuring the plugin without supplying a webpackNormalModule", () => { + beforeEach(() => { + result = fillDefaultOptionalValues({ + pointCuts: [] + }); + }); + + it("should return the NormalModule from the webpack import", () => { + expect(result.webpackNormalModule).toBe(webpack.NormalModule); + }); + }); + + describe.each` + variantGlobs | joinPointResolver | toggleHandler | description | expectation + ${undefined} | ${undefined} | ${undefined} | ${"nothing"} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler: defaultToggleHandler }} + ${variantGlobs} | ${undefined} | ${undefined} | ${"a variantGlob, but nothing else"} | ${{ variantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler: defaultToggleHandler }} + ${variantGlobs} | ${joinPointResolver} | ${undefined} | ${"a variantGlob and a join point resolver"} | ${{ variantGlobs, joinPointResolver, toggleHandler: defaultToggleHandler }} + ${undefined} | ${joinPointResolver} | ${undefined} | ${"a joinPointResolver, but nothing else"} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver, toggleHandler: defaultToggleHandler }} + ${undefined} | ${undefined} | ${toggleHandler} | ${"a toggle handler "} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler }} + ${variantGlobs} | ${undefined} | ${toggleHandler} | ${"a toggle handler and a variantGlob, but nothing else"} | ${{ variantGlobs, joinPointResolver: defaultJoinPointResolver, toggleHandler }} + ${variantGlobs} | ${joinPointResolver} | ${toggleHandler} | ${"a toggle handler, a variantGlob and a join point resolver"} | ${{ variantGlobs, joinPointResolver, toggleHandler }} + ${undefined} | ${joinPointResolver} | ${toggleHandler} | ${"a toggle handler and a joinPointResolver, but nothing else"} | ${{ variantGlobs: defaultVariantGlobs, joinPointResolver, toggleHandler }} + `( + "when configuring pointCuts, supplying $description", + // eslint-disable-next-line no-unused-vars + ({ expectation, description, ...pointCut }) => { + beforeEach(async () => { + result = fillDefaultOptionalValues({ pointCuts: [pointCut] }); + }); + + it("should fill the defaults", () => { + expect(result.pointCuts[0]).toEqual(expectation); + }); + + if (!joinPointResolver) { + makeDefaultJoinPointResolverAssertions(); + } + } + ); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/index.js b/packages/webpack/src/plugins/togglePointInjection/index.js index 12d32da..c78aa96 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.js @@ -1,11 +1,12 @@ import processPointCuts from "./processPointCuts/index.js"; import Logger from "./logger.js"; -import { win32, posix } from "path"; +import { sep, posix } from "path"; import { PLUGIN_NAME } from "./constants.js"; import resolveJoinPoints from "./resolveJoinPoints/index.js"; import setupSchemeModules from "./setupSchemeModules/index.js"; import { validate } from "schema-utils"; import schema from "./schema.json"; +import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; /** * Toggle Point Injection Plugin @@ -21,8 +22,8 @@ class TogglePointInjection { * @param {string} options.pointCuts[].togglePointModule path, from root of the compilation, of where the toggle point sits. Or a resolvable node_module. * @param {string[]} [options.pointCuts[].variantGlobs=[.\/**\/__variants__/*\/*\/!(*.test).{js,jsx,ts,tsx}]] {@link https://en.wikipedia.org/wiki/Glob_(programming)|Globs} to identified variant modules. The plugin uses {@link https://github.com/mrmlnc/fast-glob|fast-glob} under the hood, so supports any glob that it does. * @param {function} [options.pointCuts[].joinPointResolver=(variantPath) => path.posix.resolve(variantPath, "../../../..", path.basename(variantPath))] A function that takes the path to a variant module and returns a join point / base module. N.B. This is executed at build-time, so cannot use run-time context. It should use posix path segments, so on Windows be sure to use path.posix.resolve. - * @param {string} [options.pointCuts[].toggleHandler] Path to a toggle handler that unpicks a {@link https://webpack.js.org/api/module-methods/#requirecontext|require.context} containing potential variants, passing that plus a joint point module to a toggle point function. If not provided, the plugin will use a default handler that processes folder names into a tree held in a Map. Leaf nodes of the tree are the variant modules. - * @param {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 + * @param {string} [options.pointCuts[].toggleHandler='@asos/web-toggle-point-webpack/pathSegmentToggleHandler'] a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_features_into_your_script|module specifier} pointing to a toggle handler, that takes a toggle point, a Map of relative paths to potential variants, and a join point. If not provided, the plugin will use a default handler that processes folder names into a tree held in a Map. + * @param {function} [options.webpackNormalModule] A reference to the Webpack NormalModule class. This is required for Next.js, as it does not expose the NormalModule class directly * @returns {external:Webpack.WebpackPluginInstance} WebpackPluginInstance * @example N.B. forward slashes are escaped in the glob, due to JSDoc shortcomings, but in reality should be un-escaped * const plugin = new TogglePointInjection({ @@ -44,23 +45,19 @@ 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) { let NormalModule, joinPointFiles, warnings, appRoot; compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, async () => { - appRoot = compiler.context.replaceAll(win32.sep, posix.sep); + appRoot = compiler.context.replaceAll(sep, posix.sep); ({ joinPointFiles, warnings } = await processPointCuts({ appRoot, fileSystem: compiler.inputFileSystem, options: this.options })); - NormalModule = await this.options.webpackNormalModule(); + NormalModule = await this.options.webpackNormalModule; }); compiler.hooks.compilation.tap( diff --git a/packages/webpack/src/plugins/togglePointInjection/index.test.js b/packages/webpack/src/plugins/togglePointInjection/index.test.js index 472063b..d2db6bb 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.test.js @@ -4,7 +4,7 @@ import { PLUGIN_NAME } from "./constants.js"; import resolveJoinPoints from "./resolveJoinPoints/index.js"; import setupSchemeModules from "./setupSchemeModules/index.js"; import TogglePointInjection from "./index.js"; -import webpack from "webpack"; +import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; import { validate } from "schema-utils"; import schema from "./schema.json"; @@ -20,9 +20,15 @@ jest.mock("./constants", () => ({ jest.mock("./processPointCuts/index.js", () => jest.fn()); jest.mock("./resolveJoinPoints/index.js", () => jest.fn()); jest.mock("./setupSchemeModules/index.js", () => jest.fn()); -jest.mock("webpack", () => ({ NormalModule: Symbol("test-normal-module") })); jest.mock("schema-utils", () => ({ validate: jest.fn() })); jest.mock("./schema.json", () => Symbol("test-json")); +const mockNormalModule = Symbol("test-normal-module"); +jest.mock("./fillDefaultOptionalValues.js", () => + jest.fn((options) => ({ + ...options, + webpackNormalModule: mockNormalModule + })) +); describe("togglePointInjection", () => { let togglePointInjection, compiler, options; @@ -40,162 +46,145 @@ describe("togglePointInjection", () => { }; }); - const makeCommonAssertions = (NormalModule) => { - it("should validate the supplied options", () => { - expect(validate).toHaveBeenCalledWith( - schema, - options, - expect.objectContaining({ name: PLUGIN_NAME, baseDataPath: "options" }) - ); - }); - - it("should tap into the beforeCompile event, indicating the plugin name", () => { - expect(compiler.hooks.beforeCompile.tapPromise).toHaveBeenCalledWith( - PLUGIN_NAME, - expect.any(Function) - ); - }); - - it("should tap into the compilation event, indicating the plugin name", () => { - expect(compiler.hooks.compilation.tap).toHaveBeenCalledWith( - PLUGIN_NAME, - expect.any(Function) - ); + describe("when a webpackNormalModule option is not supplied", () => { + beforeEach(() => { + options = { pointCuts }; + togglePointInjection = new TogglePointInjection(options); }); - describe("when the beforeCompile event is triggered", () => { - let beforeCompileCallback, resolve, result; - const warnings = Symbol("test-warnings"); - const compilation = Symbol("test-compilation"); - const normalModuleFactory = Symbol("test-normal-module-factory"); - + describe("when applying to a compiler", () => { beforeEach(() => { - processPointCuts.mockReturnValue( - new Promise((res) => { - resolve = res; + togglePointInjection.apply(compiler); + }); + + it("should validate the supplied options", () => { + expect(validate).toHaveBeenCalledWith( + schema, + options, + expect.objectContaining({ + name: PLUGIN_NAME, + baseDataPath: "options" }) ); - [, beforeCompileCallback] = - compiler.hooks.beforeCompile.tapPromise.mock.lastCall; - result = beforeCompileCallback(); }); - it("should process the supplied point cuts, returning only when processed", () => { - expect(processPointCuts).toHaveBeenCalledWith({ - appRoot: compiler.context, - fileSystem: compiler.inputFileSystem, - options: togglePointInjection.options - }); + it("should fill in default optional values", () => { + expect(fillDefaultOptionalValues).toHaveBeenCalledWith(options); }); - const makeCommonAssertions = () => { - it("should create a logger for the compilation", () => { - expect(Logger).toHaveBeenCalledWith(compilation); - }); + it("should tap into the beforeCompile event, indicating the plugin name", () => { + expect(compiler.hooks.beforeCompile.tapPromise).toHaveBeenCalledWith( + PLUGIN_NAME, + expect.any(Function) + ); + }); + + it("should tap into the compilation event, indicating the plugin name", () => { + expect(compiler.hooks.compilation.tap).toHaveBeenCalledWith( + PLUGIN_NAME, + expect.any(Function) + ); + }); - it("should log any warnings", () => { - expect(Logger.mock.instances[0].logWarnings).toHaveBeenCalledWith( - warnings + describe("when the beforeCompile event is triggered", () => { + let beforeCompileCallback, resolve, result; + const warnings = Symbol("test-warnings"); + const compilation = Symbol("test-compilation"); + const normalModuleFactory = Symbol("test-normal-module-factory"); + + beforeEach(() => { + processPointCuts.mockReturnValue( + new Promise((res) => { + resolve = res; + }) ); + [, beforeCompileCallback] = + compiler.hooks.beforeCompile.tapPromise.mock.lastCall; + result = beforeCompileCallback(); }); - }; - - describe("when the point cuts are processed, and some are found", () => { - const joinPointFiles = new Set([Symbol("test-join-point-file")]); - beforeEach(async () => { - resolve({ joinPointFiles, warnings }); - await result; + it("should process the supplied point cuts, returning only when processed", () => { + expect(processPointCuts).toHaveBeenCalledWith({ + appRoot: compiler.context, + fileSystem: compiler.inputFileSystem, + options: togglePointInjection.options + }); }); - describe("when the compilation event is triggered", () => { - beforeEach(() => { - const [, compilationCallback] = - compiler.hooks.compilation.tap.mock.lastCall; - compilationCallback(compilation, { normalModuleFactory }); + const makeCommonAssertions = () => { + it("should create a logger for the compilation", () => { + expect(Logger).toHaveBeenCalledWith(compilation); }); - makeCommonAssertions(); - - it("should log the join points", () => { - expect(Logger.mock.instances[0].logJoinPoints).toHaveBeenCalledWith( - joinPointFiles + it("should log any warnings", () => { + expect(Logger.mock.instances[0].logWarnings).toHaveBeenCalledWith( + warnings ); }); + }; - it("should set up scheme modules", () => { - expect(setupSchemeModules).toHaveBeenCalledWith({ - NormalModule, - compilation, - joinPointFiles, - pointCuts - }); - }); + describe("when the point cuts are processed, and some are found", () => { + const joinPointFiles = new Set([Symbol("test-join-point-file")]); - it("should resolve the join points", () => { - expect(resolveJoinPoints).toHaveBeenCalledWith({ - compilation, - appRoot: compiler.context, - normalModuleFactory, - joinPointFiles - }); + beforeEach(async () => { + resolve({ joinPointFiles, warnings }); + await result; }); - }); - }); - - describe("when the point cuts are processed, and none are found", () => { - beforeEach(async () => { - resolve({ joinPointFiles: [], warnings }); - await result; - }); - describe("when the compilation event is triggered", () => { - beforeEach(() => { - const [, compilationCallback] = - compiler.hooks.compilation.tap.mock.lastCall; - compilationCallback(compilation, { - normalModuleFactory + describe("when the compilation event is triggered", () => { + beforeEach(() => { + const [, compilationCallback] = + compiler.hooks.compilation.tap.mock.lastCall; + compilationCallback(compilation, { normalModuleFactory }); }); - }); - makeCommonAssertions(); - }); - }); - }); - }; + makeCommonAssertions(); - describe("when a webpackNormalModule option is not supplied", () => { - beforeEach(() => { - options = { pointCuts }; - togglePointInjection = new TogglePointInjection(options); - }); + it("should log the join points", () => { + expect( + Logger.mock.instances[0].logJoinPoints + ).toHaveBeenCalledWith(joinPointFiles); + }); - describe("when applying to a compiler", () => { - beforeEach(() => { - togglePointInjection.apply(compiler); - }); + it("should set up scheme modules", () => { + expect(setupSchemeModules).toHaveBeenCalledWith({ + NormalModule: mockNormalModule, + compilation, + joinPointFiles, + pointCuts + }); + }); - makeCommonAssertions(webpack.NormalModule); - }); - }); + it("should resolve the join points", () => { + expect(resolveJoinPoints).toHaveBeenCalledWith({ + compilation, + appRoot: compiler.context, + normalModuleFactory, + joinPointFiles + }); + }); + }); + }); - describe("when a webpackNormalModule option is supplied (primarily for NextJs users to get around the fact that webpack is pre-bundled)", () => { - const MockNormalModule = Symbol("test-normal-module"); + describe("when the point cuts are processed, and none are found", () => { + beforeEach(async () => { + resolve({ joinPointFiles: [], warnings }); + await result; + }); - beforeEach(() => { - options = { - pointCuts, - webpackNormalModule: async () => MockNormalModule - }; - togglePointInjection = new TogglePointInjection(options); - }); + describe("when the compilation event is triggered", () => { + beforeEach(() => { + const [, compilationCallback] = + compiler.hooks.compilation.tap.mock.lastCall; + compilationCallback(compilation, { + normalModuleFactory + }); + }); - describe("when applying to a compiler", () => { - beforeEach(() => { - togglePointInjection.apply(compiler); + makeCommonAssertions(); + }); + }); }); - - makeCommonAssertions(MockNormalModule); }); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/integration.test.js b/packages/webpack/src/plugins/togglePointInjection/integration.test.js index d28aa06..07ec825 100644 --- a/packages/webpack/src/plugins/togglePointInjection/integration.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/integration.test.js @@ -1,6 +1,6 @@ import { build } from "webpack-test-utils"; import { readFile } from "fs/promises"; -import { resolve } from "path"; +import { posix } from "path"; import TogglePointInjection from "./index.js"; import { PLUGIN_NAME } from "./constants.js"; @@ -34,7 +34,7 @@ describe("togglePointInjection", () => { fileSystem = { "node_modules/@asos/web-toggle-point-webpack/pathSegmentToggleHandler": await readFile( - resolve( + posix.resolve( __dirname, "..", "..", @@ -102,7 +102,7 @@ describe("togglePointInjection", () => { it("should log the fact that the toggle point was found", () => { expect(getLogOfType("info")).toEqual( - `Identified '${name}' point cut for join point '${modulesFolder}${moduleName}' with potential variants:\n./${variantsFolder}/${testFeature}/${testVariant}/${moduleName}` + `Identified '${name}' point cut for join point '${modulesFolder}${moduleName}' with potential variants:\n${modulesFolder}${variantsFolder}/${testFeature}/${testVariant}/${moduleName}` ); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/logger.js b/packages/webpack/src/plugins/togglePointInjection/logger.js index 07ad8da..d85b6b5 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.js @@ -11,14 +11,14 @@ class Logger { for (const [ joinPoint, { - variants, + variantPathMap, pointCut: { name } } ] of joinPointFiles.entries()) { this.#logger.info( - `Identified '${name}' point cut for join point '${joinPoint}' with potential variants:\n${variants.join( - "\n" - )}` + `Identified '${name}' point cut for join point '${joinPoint}' with potential variants:\n${[ + ...variantPathMap.values() + ].join("\n")}` ); } } diff --git a/packages/webpack/src/plugins/togglePointInjection/logger.test.js b/packages/webpack/src/plugins/togglePointInjection/logger.test.js index 915f876..6306bfe 100644 --- a/packages/webpack/src/plugins/togglePointInjection/logger.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/logger.test.js @@ -21,12 +21,15 @@ describe("logger", () => { describe("logJoinPoints", () => { const pointCut = { name: "test-point-cut" }; const joinPointName = "test-join-point"; - const variants = ["test-variant-1", "test-variant-2"]; + const variantPathMap = new Map([ + ["test-key-1", "test-path-1"], + ["test-key-2", "test-key-2"] + ]); const joinPointFiles = new Map([ [ joinPointName, { - variants, + variantPathMap, pointCut: { name: "test-point-cut" } } ] @@ -40,9 +43,9 @@ describe("logger", () => { expect(compilationLogger.info).toHaveBeenCalledWith( `Identified '${ pointCut.name - }' point cut for join point '${joinPointName}' with potential variants:\n${variants.join( - "\n" - )}` + }' point cut for join point '${joinPointName}' with potential variants:\n${Array.from( + variantPathMap.values() + ).join("\n")}` ); }); }); diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js deleted file mode 100644 index fecdcd5..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.js +++ /dev/null @@ -1,13 +0,0 @@ -import { posix, basename } from "path"; - -const fillPointCutDefaults = (pointCut) => { - const { - variantGlobs = ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], - joinPointResolver = (variantPath) => - posix.resolve(variantPath, ...Array(4).fill(".."), basename(variantPath)) - } = pointCut; - - return { variantGlobs, joinPointResolver }; -}; - -export default fillPointCutDefaults; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js deleted file mode 100644 index a199b19..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/fillDefaultOptionalValues.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import fillPointCutDefaults from "./fillDefaultOptionalValues"; - -describe("fillDefaultOptionalValues", () => { - let result; - - const makeDefaultJoinPointResolverAssertions = () => { - describe("when the joinPointResolver is called", () => { - it("should return a path that is the same as the variantPath, but with the last 3 directories removed", () => { - const variantPath = - "/test-folder/test-sub-folder/test-sub-folder/test-sub-folder/test-variant"; - const joinPointPath = result.joinPointResolver(variantPath); - expect(joinPointPath).toEqual("/test-folder/test-variant"); - }); - }); - }; - - describe("when the point cut has no variantGlobs or joinPointResolver", () => { - const pointCut = {}; - - beforeEach(() => { - result = fillPointCutDefaults(pointCut); - }); - - it("should fill the defaults", () => { - expect(result).toEqual({ - variantGlobs: ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], - joinPointResolver: expect.any(Function) - }); - }); - - makeDefaultJoinPointResolverAssertions(); - }); - - describe("when the point cut has a variantGlobs but no joinPointResolver", () => { - const pointCut = { variantGlobs: Symbol("test-variant-globs") }; - - beforeEach(() => { - result = fillPointCutDefaults(pointCut); - }); - - it("should fill the defaults", () => { - expect(result).toEqual({ - variantGlobs: pointCut.variantGlobs, - joinPointResolver: expect.any(Function) - }); - }); - - makeDefaultJoinPointResolverAssertions(); - }); - - describe("when the point cut has a joinPointResolver but no variantGlobs", () => { - it("should return the supplied joinPointResolver and fill default variantGlobs", () => { - const pointCut = { - joinPointResolver: Symbol("test-join-point-resolver") - }; - const result = fillPointCutDefaults(pointCut); - expect(result).toEqual({ - variantGlobs: ["./**/__variants__/*/*/!(*.test).{js,jsx,ts,tsx}"], - joinPointResolver: pointCut.joinPointResolver - }); - }); - }); - - describe("when the point cut has variantGlobs and a joinPointResolver", () => { - it("should return the point cut supplied values", () => { - const pointCut = { - variantGlobs: Symbol("test-variant-glob"), - joinPointResolver: Symbol("test-join-point-resolver") - }; - const result = fillPointCutDefaults(pointCut); - expect(result).toEqual(pointCut); - }); - }); -}); diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js index 503b057..ec0d590 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.js @@ -1,6 +1,5 @@ import processVariantFiles from "./processVariantFiles/index.js"; import getVariantPaths from "./getVariantPaths.js"; -import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; const processPointCuts = async ({ appRoot, @@ -11,8 +10,7 @@ const processPointCuts = async ({ const configFiles = new Map(); const warnings = []; for await (const pointCut of pointCuts.values()) { - const { variantGlobs, joinPointResolver } = - fillDefaultOptionalValues(pointCut); + const { variantGlobs } = pointCut; const variantPaths = await getVariantPaths({ variantGlobs, @@ -24,7 +22,6 @@ const processPointCuts = async ({ variantPaths, joinPointFiles, pointCut, - joinPointResolver, warnings, configFiles, fileSystem, diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js index 62768a2..71f9ecd 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/index.test.js @@ -1,18 +1,11 @@ import processVariantFiles from "./processVariantFiles/index.js"; import getVariantPaths from "./getVariantPaths.js"; import processPointCuts from "./index.js"; -import fillDefaultOptionalValues from "./fillDefaultOptionalValues.js"; jest.mock("./processVariantFiles/index", () => jest.fn()); jest.mock("./getVariantPaths", () => jest.fn(() => Symbol("test-variant-files")) ); -jest.mock("./fillDefaultOptionalValues", () => - jest.fn(() => ({ - variantGlobs: Symbol("test-variant-globs"), - joinPointResolver: Symbol("test-join-point-resolver") - })) -); describe("processPointCuts", () => { const pointCuts = new Map([ @@ -34,16 +27,8 @@ describe("processPointCuts", () => { })); }); - it("should fill in default optional values for each point cut", () => { - for (const pointCut of pointCutsValues) { - expect(fillDefaultOptionalValues).toHaveBeenCalledWith(pointCut); - } - }); - it("should get variant files for each of the point cuts", () => { - for (const index of pointCutsValues.keys()) { - const { variantGlobs } = - fillDefaultOptionalValues.mock.results[index].value; + for (const { variantGlobs } of pointCutsValues.keys()) { expect(getVariantPaths).toHaveBeenCalledWith({ variantGlobs, appRoot, @@ -55,13 +40,10 @@ describe("processPointCuts", () => { it("should process the variant files, and keep a shared record of config files found between each point cut", () => { for (const [index, pointCut] of pointCutsValues.entries()) { const variantPaths = getVariantPaths.mock.results[index].value; - const { joinPointResolver } = - fillDefaultOptionalValues.mock.results[index].value; expect(processVariantFiles).toHaveBeenCalledWith({ variantPaths, joinPointFiles, pointCut, - joinPointResolver, warnings, configFiles: expect.any(Map), fileSystem, diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js index 8d8e09a..33e4b5d 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.js @@ -1,17 +1,20 @@ import { posix } from "path"; -import isJoinPointInvalid from "./isJoinPointInvalid"; +import isJoinPointInvalid from "./isJoinPointInvalid.js"; +import linkJoinPoints from "./linkJoinPoints.js"; const { parse, relative } = posix; +const normalizeToRelativePath = (path, joinDirectory) => + relative(joinDirectory, path).replace(/^([^./])/, "./$1"); + const processVariantFiles = async ({ variantPaths, joinPointFiles, pointCut, - joinPointResolver, warnings, ...rest }) => { for (const variantPath of variantPaths) { - const joinPointPath = joinPointResolver(variantPath); + const joinPointPath = pointCut.joinPointResolver(variantPath); const { dir: directory, base: filename } = parse(joinPointPath); if (!joinPointFiles.has(joinPointPath)) { @@ -27,7 +30,7 @@ const processVariantFiles = async ({ } joinPointFiles.set(joinPointPath, { pointCut, - variants: [] + variantPathMap: new Map() }); } @@ -39,10 +42,11 @@ const processVariantFiles = async ({ continue; } - joinPointFile.variants.push( - relative(directory, variantPath).replace(/^([^./])/, "./$1") - ); + const key = normalizeToRelativePath(variantPath, directory); + joinPointFile.variantPathMap.set(key, variantPath); } + + linkJoinPoints(joinPointFiles); }; export default processVariantFiles; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js index 1ff96c5..4a2a4c2 100644 --- a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/index.test.js @@ -1,41 +1,37 @@ -import processVariantFiles from "."; -import { memfs } from "memfs"; import { posix } from "path"; +import isJoinPointInvalid from "./isJoinPointInvalid.js"; +import linkJoinPoints from "./linkJoinPoints.js"; +import processVariantFiles from "./index.js"; + const { resolve, join, sep } = posix; +jest.mock("./linkJoinPoints.js", () => jest.fn()); +jest.mock("./isJoinPointInvalid.js", () => jest.fn()); + describe("processVariantFiles", () => { let joinPointFiles; - const pointCut = { name: "test-point-cut" }; - const joinPointResolver = jest.fn(); + const pointCut = { name: "test-point-cut", joinPointResolver: jest.fn() }; let warnings; const variantFileGlob = "test-variant-*.*"; - const variantGlobs = [`/${variantFileGlob}`]; - const appRoot = "/test-app-root/"; const moduleFile = "test-module.js"; const joinPointFolder = "test-folder"; const joinPointPath = join(joinPointFolder, moduleFile); - const { fs: fileSystem } = memfs({ - [`${appRoot}${joinPointPath}`]: "join point" - }); + const rest = { [Symbol("test-key")]: Symbol("test-value") }; - beforeEach(() => { + beforeEach(async () => { + jest.clearAllMocks(); warnings = []; joinPointFiles = new Map(); }); - const act = async ({ variantPaths, configFiles }) => { + const act = async ({ variantPaths }) => { await processVariantFiles({ variantPaths, - configFiles, joinPointFiles, pointCut, - joinPointResolver, - variantGlobs, warnings, - name: moduleFile, - fileSystem, - appRoot + ...rest }); }; @@ -61,14 +57,41 @@ describe("processVariantFiles", () => { `( "when given a variant path ($variantPath)", ({ variantPath, expectedVariant }) => { + const path = resolve(joinPointFolder, variantPath); const variantPaths = new Set([resolve(joinPointFolder, variantPath)]); - describe("when given a variant file that has no matching join point file", () => { + const makeCommonAssertions = () => { + it("should call the joinPointResolver with the path to the variant file", () => { + expect(pointCut.joinPointResolver).toHaveBeenCalledWith(path); + }); + + it("should call linkJoinPoints with the updated joinPointFiles", () => { + expect(linkJoinPoints).toHaveBeenCalledWith(joinPointFiles); + }); + }; + + describe("when given a variant file that is not valid according to the configured config files", () => { + const filename = "test-not-matching-control"; + const joinPointPath = join(joinPointFolder, filename); + beforeEach(async () => { - joinPointResolver.mockReturnValue( - join(joinPointFolder, "test-not-matching-control") - ); - await act({ variantPaths, configFiles: new Map() }); + pointCut.joinPointResolver.mockReturnValue(joinPointPath); + isJoinPointInvalid.mockReturnValue(true); + await act({ + variantPaths, + configFiles: new Map() + }); + }); + + makeCommonAssertions(); + + it("should call isJoinPointInvalid with the expected arguments", () => { + expect(isJoinPointInvalid).toHaveBeenCalledWith({ + filename, + joinPointPath, + directory: joinPointFolder, + ...rest + }); }); it("should add no warnings, and not modify joinPointFiles", async () => { @@ -79,41 +102,27 @@ describe("processVariantFiles", () => { describe("when given a variant file that has a matching join point file", () => { beforeEach(async () => { - joinPointResolver.mockReturnValue(joinPointPath); + pointCut.joinPointResolver.mockReturnValue(joinPointPath); }); describe("and no config file precludes it being valid", () => { beforeEach(async () => { - await act({ variantPaths, configFiles: new Map() }); + isJoinPointInvalid.mockReturnValue(false); + await act({ variantPaths }); }); - it("should add no warnings, and add a single joinPointFile representing the matched join point", async () => { - expect(warnings).toEqual([]); - expect(joinPointFiles).toEqual( - new Map([ - [ - joinPointPath, - { - pointCut, - variants: [expectedVariant] - } - ] - ]) - ); - }); - }); + makeCommonAssertions(); - describe("and a config file confirms it as valid", () => { - beforeEach(async () => { - await act({ - variantPaths, - configFiles: new Map([ - [joinPointFolder, { joinPoints: [moduleFile] }] - ]) + it("should call isJoinPointInvalid with the expected arguments", () => { + expect(isJoinPointInvalid).toHaveBeenCalledWith({ + filename: moduleFile, + joinPointPath, + directory: joinPointFolder, + ...rest }); }); - it("should add no warnings, and add a single joinPointFile representing the matched join point", async () => { + it("should add no warnings, and add a single joinPointFile representing the matched join point, relative to the control module", async () => { expect(warnings).toEqual([]); expect(joinPointFiles).toEqual( new Map([ @@ -121,7 +130,7 @@ describe("processVariantFiles", () => { joinPointPath, { pointCut, - variants: [expectedVariant] + variantPathMap: new Map([[expectedVariant, path]]) } ] ]) @@ -129,20 +138,6 @@ describe("processVariantFiles", () => { }); }); - describe("and a config file precludes it from being valid", () => { - beforeEach(async () => { - await act({ - variantPaths, - configFiles: new Map([[joinPointFolder, { joinPoints: [] }]]) - }); - }); - - it("should add no warnings, and not modify joinPointFiles", async () => { - expect(warnings).toEqual([]); - expect(joinPointFiles).toEqual(new Map()); - }); - }); - describe("and a preceding point cut already identified the join point", () => { const testOtherPointCut = { name: "test-other-point-cut" }; beforeEach(async () => { @@ -150,11 +145,16 @@ describe("processVariantFiles", () => { pointCut: testOtherPointCut }); await act({ - variantPaths, - configFiles: new Map() + variantPaths }); }); + makeCommonAssertions(); + + it("should not check if the join point is invalid again", () => { + expect(isJoinPointInvalid).not.toHaveBeenCalled(); + }); + it("should add a warning, and not modify joinPointFiles", async () => { expect(warnings).toEqual([ `Join point "${joinPointPath}" is already assigned to point cut "${testOtherPointCut.name}". Skipping assignment to "${pointCut.name}".` diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.js new file mode 100644 index 0000000..4b6dd65 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.js @@ -0,0 +1,13 @@ +import { JOIN_POINTS, SCHEME } from "../../constants.js"; + +const linkJoinPoints = (joinPointFiles) => { + for (const [, { variantPathMap }] of joinPointFiles) { + for (const [key, path] of variantPathMap) { + if (joinPointFiles.has(path)) { + variantPathMap.set(key, `${SCHEME}:${JOIN_POINTS}:${path}`); + } + } + } +}; + +export default linkJoinPoints; diff --git a/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.test.js b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.test.js new file mode 100644 index 0000000..ad8b4ff --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/processPointCuts/processVariantFiles/linkJoinPoints.test.js @@ -0,0 +1,109 @@ +import linkJoinPoints from "./linkJoinPoints"; +import { JOIN_POINTS, SCHEME } from "../../constants.js"; + +jest.mock("../../constants.js", () => ({ + JOIN_POINTS: "mockedJoinPoints", + SCHEME: "mockedScheme" +})); + +describe("linkJoinPoints", () => { + let mockJoinPoint1, mockJoinPoint2; + beforeEach(() => { + mockJoinPoint1 = [ + "path/to/joinPoint1", + { + variantPathMap: new Map([ + ["path/to/jointPoint1/variant1", "path/to/jointPoint1/variant1"], + ["path/to/jointPoint1/variant2", "path/to/jointPoint1/variant2"] + ]) + } + ]; + mockJoinPoint2 = [ + "path/to/joinPoint2", + { + variantPathMap: new Map([ + ["path/to/jointPoint2/variant1", "path/to/jointPoint2/variant1"], + ["path/to/jointPoint2/variant2", "path/to/jointPoint2/variant2"] + ]) + } + ]; + }); + + describe("when join points are not chained", () => { + it("should not modify the join point files", () => { + const joinPointFiles = new Map([mockJoinPoint1, mockJoinPoint2]); + linkJoinPoints(joinPointFiles); + + expect(joinPointFiles).toEqual( + new Map([ + [ + "path/to/joinPoint1", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint1/variant1", + "path/to/jointPoint1/variant1" + ], + ["path/to/jointPoint1/variant2", "path/to/jointPoint1/variant2"] + ]) + } + ], + [ + "path/to/joinPoint2", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint2/variant1", + "path/to/jointPoint2/variant1" + ], + ["path/to/jointPoint2/variant2", "path/to/jointPoint2/variant2"] + ]) + } + ] + ]) + ); + }); + }); + + describe("when a variant of a join point is itself a join point", () => { + it("should link variants of the join point to the connected join point", () => { + const connectedJoinPoint2 = [ + "path/to/jointPoint1/variant1", + mockJoinPoint2[1] + ]; + + const joinPointFiles = new Map([mockJoinPoint1, connectedJoinPoint2]); + + linkJoinPoints(joinPointFiles); + + expect(joinPointFiles).toEqual( + new Map([ + [ + "path/to/joinPoint1", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint1/variant1", + `${SCHEME}:${JOIN_POINTS}:path/to/jointPoint1/variant1` + ], + ["path/to/jointPoint1/variant2", "path/to/jointPoint1/variant2"] + ]) + } + ], + [ + "path/to/jointPoint1/variant1", + { + variantPathMap: new Map([ + [ + "path/to/jointPoint2/variant1", + "path/to/jointPoint2/variant1" + ], + ["path/to/jointPoint2/variant2", "path/to/jointPoint2/variant2"] + ]) + } + ] + ]) + ); + }); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js index 9f36121..40e24f2 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js @@ -1,5 +1,5 @@ import { PLUGIN_NAME } from "../constants"; -import { posix, win32 } from "path"; +import { sep, posix } from "path"; import { promisify } from "util"; import handleJoinPointMatch from "./handleJoinPointMatch"; const { relative } = posix; @@ -7,6 +7,33 @@ const { relative } = posix; const isLoaderlessFileRequest = (request) => [".", "/"].includes(request.at(0)) && !request.includes("!"); +const matchJoinPointIfResolved = async ({ + enhancedResolve, + resolveData, + appRoot, + joinPointFiles, + compilation +}) => { + const resolved = await enhancedResolve( + {}, + resolveData.context, + resolveData.request, + {} + ); + if (!resolved) { + return; + } + + const resource = `/${relative(appRoot, resolved.replaceAll(sep, posix.sep))}`; + if (joinPointFiles.has(resource)) { + handleJoinPointMatch({ + resource, + compilation, + resolveData + }); + } +}; + const resolveJoinPoints = ({ compilation, appRoot, @@ -23,29 +50,19 @@ const resolveJoinPoints = ({ async (resolveData) => { if ( !joinPointFiles.size || - !resolveData.context - .replaceAll(win32.sep, posix.sep) - .startsWith(appRoot) || + !resolveData.context.replaceAll(sep, posix.sep).startsWith(appRoot) || !isLoaderlessFileRequest(resolveData.request) ) { return; } - const resolved = await enhancedResolve( - {}, - resolveData.context, - resolveData.request, - {} - ); - - const resource = `/${relative(appRoot, resolved.replaceAll(win32.sep, posix.sep))}`; - if (joinPointFiles.has(resource)) { - handleJoinPointMatch({ - resource, - compilation, - resolveData - }); - } + await matchJoinPointIfResolved({ + appRoot, + joinPointFiles, + enhancedResolve, + resolveData, + compilation + }); } ); }; diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js index e9628c8..b08d897 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js @@ -1,7 +1,7 @@ import { PLUGIN_NAME } from "../constants"; import handleJoinPointMatch from "./handleJoinPointMatch"; import { sep, join } from "path"; -import resolvePointCuts from "."; +import resolveJoinPoints from "."; jest.mock("../constants", () => ({ PLUGIN_NAME: "test-plugin-name" @@ -48,7 +48,7 @@ describe("resolveJoinPoints", () => { const joinPointFiles = new Map(); beforeEach(() => { - resolvePointCuts({ + resolveJoinPoints({ compilation, appRoot, normalModuleFactory, @@ -64,7 +64,7 @@ describe("resolveJoinPoints", () => { beforeEach(() => { [, beforeResolveCallback] = normalModuleFactory.hooks.beforeResolve.tapPromise.mock.lastCall; - handleJoinPointMatch.mockClear(); + jest.clearAllMocks(); beforeResolveCallback(); }); @@ -77,7 +77,7 @@ describe("resolveJoinPoints", () => { describe("when there are some join points previously identified", () => { const joinPointFile = "/test-folder/test-join-point-file"; beforeEach(() => { - resolvePointCuts({ + resolveJoinPoints({ compilation, appRoot, normalModuleFactory, @@ -157,6 +157,19 @@ describe("resolveJoinPoints", () => { }); }; + describe("and the file cannot be resolved", () => { + beforeEach(() => { + mockResolvedResource = false; + beforeResolveCallback(resolveData); + }); + + makeCommonAssertions(); + + it("should not try to handle a match", () => { + expect(handleJoinPointMatch).not.toHaveBeenCalled(); + }); + }); + describe("and the file is not a join point", () => { beforeEach(() => { mockResolvedResource = join(appRoot, "some-other-file"); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js deleted file mode 100644 index cab2b71..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.js +++ /dev/null @@ -1,20 +0,0 @@ -import { posix } from "path"; -import regexgen from "regexgen"; -import { POINT_CUTS, SCHEME } from "../constants.js"; -const { dirname } = posix; - -const generateJoinPoint = ({ joinPointFiles, path }) => { - const { - pointCut: { name }, - variants - } = joinPointFiles.get(path); - const directory = dirname(path); - const regex = regexgen(variants); - - return `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}"; -import * as joinPoint from "${path}"; -const variants = import.meta.webpackContext("${directory}", { recursive: true, regExp: ${regex} }); -export default pointCut({ joinPoint, variants });`; -}; - -export default generateJoinPoint; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js deleted file mode 100644 index d03a273..0000000 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import { POINT_CUTS, SCHEME } from "../constants.js"; -import generateJoinPoint from "./generateJoinPoint.js"; - -jest.mock("../constants", () => ({ - SCHEME: "test-scheme", - POINT_CUTS: "test-point-cuts" -})); - -describe("generateJoinPoint", () => { - const path = "test-folder/test-path"; - const pointCutName = "test-point-cut"; - const variants = [ - "test-sub-folder/test-variant-1", - "test-sub-folder/test-variant-2", - "test-other-sub-folder/test-variant-1" - ]; - let result; - - beforeEach(() => { - const joinPointFiles = new Map([ - [path, { pointCut: { name: pointCutName }, variants }] - ]); - result = generateJoinPoint({ joinPointFiles, path }); - }); - - it("should return a script that imports the appropriate point cut for the join point", () => { - expect(result).toMatch( - `import pointCut from "${SCHEME}:${POINT_CUTS}:/${pointCutName}";` - ); - }); - - it("should return a script that imports the base / control module for the join point", () => { - expect(result).toMatch(`import * as joinPoint from "${path}";`); - }); - - it("should return a script that imports all the valid variants of the base / control module into a webpackContext, using a minimal regex that matches all the variants", () => { - expect(result).toMatch( - `const variants = import.meta.webpackContext("${ - path.split("/")[0] - }", { recursive: true, regExp: /test\\-(?:other\\-sub\\-folder\\/test\\-variant\\-1|sub\\-folder\\/test\\-variant\\-[12])/ });` - ); - }); - - it("should return a script exports a default export which calls the point cut, passing the join point (control module) and the variants", () => { - expect(result).toMatch("export default pointCut({ joinPoint, variants });"); - }); -}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.js new file mode 100644 index 0000000..770367f --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.js @@ -0,0 +1,5 @@ +const createVariantPathMap = (content) => `const variantPathMap = new Map([ +${content} +]);`; + +export default createVariantPathMap; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.test.js new file mode 100644 index 0000000..cabf55a --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/createVariantPathMap.test.js @@ -0,0 +1,11 @@ +import createVariantPathMap from "./createVariantPathMap"; + +describe("createVariantPathMap", () => { + it("should return code to create a variantPathMap constant, wrapping the supplied content in a Map", () => { + const testContent = "test-content"; + expect(createVariantPathMap(testContent)) + .toEqual(`const variantPathMap = new Map([ +${testContent} +]);`); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.js new file mode 100644 index 0000000..49a6583 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.js @@ -0,0 +1,15 @@ +import createVariantPathMap from "./createVariantPathMap.js"; + +const importCodeGenerator = ({ joinPointPath, variantPathMap }) => { + const variantsKeys = Array.from(variantPathMap.keys()); + return `import * as joinPoint from "${joinPointPath}"; +${variantsKeys + .map( + (key, index) => + `import * as variant_${index} from "${variantPathMap.get(key)}";` + ) + .join("\n")} +${createVariantPathMap(variantsKeys.map((key, index) => ` ["${key}", variant_${index}]`).join(",\n"))}`; +}; + +export default importCodeGenerator; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.test.js new file mode 100644 index 0000000..f80d411 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/importCodeGenerator.test.js @@ -0,0 +1,44 @@ +import importCodeGenerator from "./importCodeGenerator"; + +describe("importCodeGenerator", () => { + const joinPointPath = "/test-folder/test-path"; + const relativePaths = [ + "/test-sub-folder/test-variant-1", + "/test-sub-folder/test-variant-2", + "/test-other-sub-folder/test-variant-1" + ]; + const variantPathMap = new Map( + relativePaths.map((relativePath) => [ + relativePath, + `${joinPointPath}${relativePath}` + ]) + ); + + let result; + + beforeEach(() => { + result = importCodeGenerator({ + joinPointPath, + variantPathMap + }); + }); + + it("should return code that imports the base / control module for the join point", () => { + expect(result).toMatch(`import * as joinPoint from "${joinPointPath}";`); + }); + + it("should return code that imports all the valid variants of the base / control module, storing in variables", () => { + expect(result).toMatch(` +import * as variant_0 from "${joinPointPath}${relativePaths[0]}"; +import * as variant_1 from "${joinPointPath}${relativePaths[1]}"; +import * as variant_2 from "${joinPointPath}${relativePaths[2]}";`); + }); + + it("should return code that creates a Map of variants, keyed by relative path, valued as the variant module", () => { + expect(result).toMatch(`const variantPathMap = new Map([ + ["/test-sub-folder/test-variant-1", variant_0], + ["/test-sub-folder/test-variant-2", variant_1], + ["/test-other-sub-folder/test-variant-1", variant_2] +]);`); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.js new file mode 100644 index 0000000..58b7ea6 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.js @@ -0,0 +1,18 @@ +import { POINT_CUTS, SCHEME } from "../../constants.js"; +import importCodeGenerator from "./importCodeGenerator.js"; + +const generateJoinPoint = ({ joinPointFiles, joinPointPath }) => { + const { + pointCut: { name }, + variantPathMap + } = joinPointFiles.get(joinPointPath); + const pointCutImport = `import pointCut from "${SCHEME}:${POINT_CUTS}:/${name}";`; + + const code = importCodeGenerator({ joinPointPath, variantPathMap }); + + return `${pointCutImport} +${code} +export default pointCut({ joinPoint, variantPathMap });`; +}; + +export default generateJoinPoint; diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.test.js new file mode 100644 index 0000000..3a114d8 --- /dev/null +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generateJoinPoint/index.test.js @@ -0,0 +1,51 @@ +import { POINT_CUTS, SCHEME } from "../../constants.js"; +import generateJoinPoint from "./index.js"; +import importCodeGenerator from "./importCodeGenerator.js"; + +jest.mock("../../constants", () => ({ + SCHEME: "test-scheme", + POINT_CUTS: "test-point-cuts" +})); +const mockImportCode = + "const joinPoint = 'test-join-point'; const variantPathMap = 'test-variants';"; +jest.mock("./importCodeGenerator.js", () => jest.fn(() => mockImportCode)); + +describe("generateJoinPoint", () => { + const joinPointPath = "/test-path"; + const pointCutName = "test-point-cut"; + const variantPathMap = Symbol("test-variant-path-map"); + const pointCut = { + name: pointCutName + }; + let result; + + beforeEach(() => { + const joinPointFiles = new Map([ + [joinPointPath, { pointCut, variantPathMap }] + ]); + result = generateJoinPoint({ + joinPointFiles, + joinPointPath + }); + }); + + it("should return a script that imports the appropriate point cut for the join point", () => { + expect(result).toMatch( + `import pointCut from "${SCHEME}:${POINT_CUTS}:/${pointCutName}";` + ); + }); + + it("should call the import code generator of the passed loading strategy, and return a script that includes the import code", () => { + expect(importCodeGenerator).toHaveBeenCalledWith({ + joinPointPath, + variantPathMap + }); + expect(result).toMatch(mockImportCode); + }); + + it("should return a script that exports a default export which calls the point cut, passing the join point (control module) and the variantPathMap returned by the import code", () => { + expect(result).toMatch( + "export default pointCut({ joinPoint, variantPathMap });" + ); + }); +}); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js index bb104dc..4361c83 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.js @@ -1,5 +1,5 @@ -const generatePointCut = ({ pointCuts, path }) => { - const pointCutName = path.slice(1); +const generatePointCut = ({ pointCuts, joinPointPath }) => { + const pointCutName = joinPointPath.slice(1); const { togglePointModule, toggleHandler = "@asos/web-toggle-point-webpack/pathSegmentToggleHandler" diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js index cbcc7d6..f825a7c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/generatePointCut.test.js @@ -2,7 +2,7 @@ import generatePointCut from "./generatePointCut.js"; describe("generatePointCut", () => { const pointCutName = "test-point-cut"; - const path = `/${pointCutName}`; + const joinPointPath = `/${pointCutName}`; const togglePointModule = "test-toggle-point-path"; let result, pointCuts; @@ -30,7 +30,7 @@ describe("generatePointCut", () => { beforeEach(() => { pointCuts[1].toggleHandler = toggleHandler; - result = generatePointCut({ pointCuts, path }); + result = generatePointCut({ pointCuts, joinPointPath }); }); it("should return a script that imports the appropriate toggle handler", () => { @@ -42,7 +42,7 @@ describe("generatePointCut", () => { describe("when a toggle handler is not configured against the point cut", () => { beforeEach(() => { - result = generatePointCut({ pointCuts, path }); + result = generatePointCut({ pointCuts, joinPointPath }); }); it("should return a script that imports the default toggle handler (a path segment toggle handler)", () => { diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js index b5d014f..db42a9c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.js @@ -1,5 +1,5 @@ import { PLUGIN_NAME, POINT_CUTS, JOIN_POINTS, SCHEME } from "../constants.js"; -import generateJoinPoint from "./generateJoinPoint.js"; +import generateJoinPoint from "./generateJoinPoint/index.js"; import generatePointCut from "./generatePointCut.js"; const setupSchemeModules = ({ @@ -11,13 +11,13 @@ const setupSchemeModules = ({ NormalModule.getCompilationHooks(compilation) .readResource.for(SCHEME) .tap(PLUGIN_NAME, ({ resourcePath }) => { - const [, type, path] = resourcePath.split(/:(.*?):(.*)/, 3); + const [, type, joinPointPath] = resourcePath.split(/:(.*?):(.*)/, 3); switch (type) { case POINT_CUTS: { - return generatePointCut({ pointCuts, path }); + return generatePointCut({ pointCuts, joinPointPath }); } case JOIN_POINTS: { - return generateJoinPoint({ joinPointFiles, path }); + return generateJoinPoint({ joinPointFiles, joinPointPath }); } } }); diff --git a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js index ca8a234..da55147 100644 --- a/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/setupSchemeModules/index.test.js @@ -1,5 +1,5 @@ import { PLUGIN_NAME, POINT_CUTS, JOIN_POINTS, SCHEME } from "../constants.js"; -import generateJoinPoint from "./generateJoinPoint.js"; +import generateJoinPoint from "./generateJoinPoint/index.js"; import generatePointCut from "./generatePointCut.js"; import setupSchemeModules from "./index.js"; @@ -46,35 +46,42 @@ describe("setupSchemeModules", () => { let result; describe("and the resource is prefixed with the point cuts type", () => { - const path = "test-point-cut-name"; + const joinPointPath = "test-point-cut-name"; const output = Symbol("test-output"); beforeEach(() => { const [, callback] = tap.mock.lastCall; generatePointCut.mockReturnValue(output); - result = callback({ resourcePath: `${SCHEME}:${POINT_CUTS}:${path}` }); + result = callback({ + resourcePath: `${SCHEME}:${POINT_CUTS}:${joinPointPath}` + }); }); it("should generate the point cut and return the generated module to the read resource hook", () => { - expect(generatePointCut).toHaveBeenCalledWith({ pointCuts, path }); + expect(generatePointCut).toHaveBeenCalledWith({ + pointCuts, + joinPointPath + }); expect(result).toBe(output); }); }); describe("and the resource is prefixed with the join points type", () => { - const path = "test-path"; + const joinPointPath = "test-path"; const output = Symbol("test-output"); beforeEach(() => { const [, callback] = tap.mock.lastCall; generateJoinPoint.mockReturnValue(output); - result = callback({ resourcePath: `${SCHEME}:${JOIN_POINTS}:${path}` }); + result = callback({ + resourcePath: `${SCHEME}:${JOIN_POINTS}:${joinPointPath}` + }); }); it("should generate a join point and return the generated module to the read resource hook", () => { expect(generateJoinPoint).toHaveBeenCalledWith({ joinPointFiles, - path + joinPointPath }); expect(result).toBe(output); }); diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js index 99a2633..5c568d3 100644 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js +++ b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.js @@ -1,9 +1,9 @@ -const buildTree = (map = new Map(), parts, variants, key) => { +const buildTree = (map = new Map(), parts, value) => { const [part, ...rest] = parts; if (rest.length) { - map.set(part, buildTree(map.get(part), rest, variants, key)); + map.set(part, buildTree(map.get(part), rest, value)); } else { - map.set(part, variants(key)); + map.set(part, value); } return map; }; @@ -15,14 +15,18 @@ const buildTree = (map = new Map(), parts, variants, key) => { * @param {object} options plugin options * @param {function} options.togglePoint a method that chooses the appropriate module at runtime * @param {module} options.joinPoint the join point module - * @param {string[]} options.variants an array of paths, as generated by webpack, that point to variants of the join point module + * @param {Map} params.variantPathMap a Map of posix file paths, relative to the join point module, to variant modules * @returns {function} A handler of join points injected by the plugin */ -const pathSegmentToggleHandler = ({ togglePoint, joinPoint, variants }) => { +const pathSegmentToggleHandler = ({ + togglePoint, + joinPoint, + variantPathMap +}) => { let featuresMap; - for (const key of variants.keys()) { + for (const [key, value] of variantPathMap) { const parts = key.split("/").slice(0, -1).slice(2); - featuresMap = buildTree(featuresMap, parts, variants, key); + featuresMap = buildTree(featuresMap, parts, value); } return togglePoint(joinPoint, featuresMap); }; diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js index 6e64730..722624e 100644 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js +++ b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js @@ -5,9 +5,7 @@ const togglePoint = jest.fn(() => toggleOutcome); const joinPoint = Symbol("mock-join-point"); describe("pathSegmentToggleHandler", () => { - let result, variantsMap; - const variants = (key) => variantsMap[key]; - variants.keys = () => Object.keys(variantsMap); + let result; beforeEach(() => { jest.clearAllMocks(); @@ -17,15 +15,25 @@ describe("pathSegmentToggleHandler", () => { const keyArray = [...Array(segmentCount).keys()]; describe(`given a list of variant paths with ${segmentCount} path segments (after the variants path)`, () => { + let variantPathMap; + beforeEach(() => { const segments = keyArray.map((key) => `test-segment-${key}/`); - variantsMap = { - [`./__variants__/${segments.join("")}test-variant.js`]: - Symbol("test-variant"), - [`./__variants__/${segments.reverse().join("")}test-variant.js`]: + variantPathMap = new Map([ + [ + `./__variants__/${segments.join("")}test-variant.js`, + Symbol("test-variant") + ], + [ + `./__variants__/${segments.reverse().join("")}test-variant.js`, Symbol("test-variant") - }; - result = pathSegmentToggleHandler({ togglePoint, joinPoint, variants }); + ] + ]); + result = pathSegmentToggleHandler({ + togglePoint, + joinPoint, + variantPathMap + }); }); it("should call the toggle point with the join point module and a map", () => { @@ -44,14 +52,14 @@ describe("pathSegmentToggleHandler", () => { }); it("should return a map containing maps for each segment, concluding with the variant at the leaf node", () => { - for (const key of Object.keys(variantsMap)) { + for (const key of Object.keys(variantPathMap)) { const segments = key.split("/").slice(0, -1); let node = map; for (const segment of segments.slice(2)) { expect(node.has(segment)).toBe(true); node = node.get(segment); } - expect(node).toBe(variantsMap[key]); + expect(node).toBe(variantPathMap[key]); } }); }); diff --git a/packages/webpack/test/test-utils.js b/packages/webpack/test/test-utils.js index 7065956..726c018 100644 --- a/packages/webpack/test/test-utils.js +++ b/packages/webpack/test/test-utils.js @@ -20,7 +20,6 @@ export const createMockGraph = ({ depth, siblingsAtEachDepthCount }) => { createGraph(rootNode, depth, siblingsAtEachDepthCount); const getIncomingConnections = jest.fn(function* (module) { - // eslint-disable-next-line jsdoc/require-jsdoc function* traverse(node) { if (node.resource === module.resource) { yield* issuersMap.get(node).map((node) => ({ From e41ef947bb543ff7cc0e29ea5dbd7512b4ac9b8b Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:25:35 +0100 Subject: [PATCH 06/44] use get() --- .../webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js index 722624e..ecf556f 100644 --- a/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js +++ b/packages/webpack/src/toggleHandlers/pathSegmentToggleHandler.test.js @@ -59,7 +59,7 @@ describe("pathSegmentToggleHandler", () => { expect(node.has(segment)).toBe(true); node = node.get(segment); } - expect(node).toBe(variantPathMap[key]); + expect(node).toBe(variantPathMap.get(key)); } }); }); From a30583bdade0eadf1df272b46a48a9f2b38e047d Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:06:43 +0100 Subject: [PATCH 07/44] fix version link in CHANGELOGs to use correct date --- examples/next/docs/CHANGELOG.md | 2 +- examples/serve/docs/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index e918e3c..dfd5a58 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- update to take supply static `webpackNormalModule` corresponding to webpack plugin [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-05-29) +- update to take supply static `webpackNormalModule` corresponding to webpack plugin [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) ## [0.2.4] - 2025-05-27 diff --git a/examples/serve/docs/CHANGELOG.md b/examples/serve/docs/CHANGELOG.md index e2ca06f..17cf9f2 100644 --- a/examples/serve/docs/CHANGELOG.md +++ b/examples/serve/docs/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- updated toggle handlers to take a `variantPathMap` corresponding to webpack [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-05-29) +- updated toggle handlers to take a `variantPathMap` corresponding to webpack [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) ## [0.2.6] - 2025-07-14 From 42a078456c7d6cef435761db3a6a85100415e753 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:10:10 +0100 Subject: [PATCH 08/44] more CHANGELOG date fix --- examples/next/docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index dfd5a58..08af4aa 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -5,7 +5,7 @@ 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.5] - 2025-05-29 +## [0.2.5] - 2025-07-29 ### Changed From 904e2e53ee862f916554fbca5976c9d3395d18cc Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:15:47 +0100 Subject: [PATCH 09/44] handle circular references --- packages/webpack/docs/CHANGELOG.md | 1 + .../resourceProxyExistsInRequestChain.js | 6 ++ .../resourceProxyExistsInRequestChain.test.js | 63 +++++++++++++------ 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index 66419da..18853e4 100644 --- a/packages/webpack/docs/CHANGELOG.md +++ b/packages/webpack/docs/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - removed "next" peer dependency, this needn't be explicit - ensured files that cannot be resolved (by [enhanced-resolve](https://github.com/webpack/enhanced-resolve/)), for whatever reason, don't break the build +- ensured that circular dependencies don't cause the module graph search lock up ## [0.8.0] - 2025-05-27 diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js index 83e8d3b..bfc049c 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js @@ -4,12 +4,18 @@ const resourceProxyExistsInRequestChain = ({ proxyResource }) => { const queue = [issuerModule]; + const visited = new Set(); while (queue.length) { const node = queue.shift(); if (node.resource === proxyResource) { return true; } + if (visited.has(node)) { + continue; + } + visited.add(node); + const incomingConnections = moduleGraph.getIncomingConnections(node); queue.push( ...new Set( diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js index 058a137..de74b7a 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.test.js @@ -1,13 +1,14 @@ import resourceProxyExistsInRequestChain from "./resourceProxyExistsInRequestChain"; import { createMockGraph } from "../../../../../test/test-utils"; -const moduleGraph = { getIncomingConnections: jest.fn() }; const proxyResource = Symbol("test-proxy-resource"); describe("resourceProxyExistsInRequestChain", () => { let result; describe("when the issuer module is the proxy resource", () => { + const moduleGraph = { getIncomingConnections: jest.fn() }; + beforeEach(() => { result = resourceProxyExistsInRequestChain({ moduleGraph, @@ -118,28 +119,54 @@ describe("resourceProxyExistsInRequestChain", () => { }); describe("and one of the modules that imported the issuer module was not imported by the proxy resource", () => { - let calls; - - beforeEach(() => { - result = resourceProxyExistsInRequestChain({ - moduleGraph, - issuerModule, - proxyResource + describe("and there is a circular dependency", () => { + beforeEach(() => { + let count = 0; + const newModuleGraph = { + ...moduleGraph, + getIncomingConnections: jest.fn().mockImplementation((module) => { + if (count++ === 10) { + count = 0; + return moduleGraph.getIncomingConnections(issuerModule); + } + return moduleGraph.getIncomingConnections(module); + }) + }; + result = resourceProxyExistsInRequestChain({ + moduleGraph: newModuleGraph, + issuerModule, + proxyResource + }); }); - calls = moduleGraph.getIncomingConnections.mock.calls; + it("should return false, without locking up / running forever", () => { + expect(result).toBe(false); + }); }); - it("should have traversed the whole import tree of the issuer module", () => { - let expectedCount = 1; - for (let level = 1; level <= depth; level++) { - expectedCount += Math.pow(siblingsAtEachDepthCount, level); - } - expect(calls.length).toEqual(expectedCount); - }); + describe("and there is no circular dependency", () => { + beforeEach(() => { + result = resourceProxyExistsInRequestChain({ + moduleGraph, + issuerModule, + proxyResource + }); + }); + + it("should have traversed the whole import tree of the issuer module", () => { + let expectedCount = 1; + for (let level = 1; level <= depth; level++) { + expectedCount += Math.pow(siblingsAtEachDepthCount, level); + } - it("should return false", () => { - expect(result).toBe(false); + expect( + moduleGraph.getIncomingConnections.mock.calls.length + ).toEqual(expectedCount); + }); + + it("should return false", () => { + expect(result).toBe(false); + }); }); }); }); From d199c1cb2f3114d866855c7b8b1e0741bf9f8841 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:29:36 +0100 Subject: [PATCH 10/44] try being explicit about type import? --- test/automation/base.config.ts | 2 +- test/automation/docs/CHANGELOG.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/automation/base.config.ts b/test/automation/base.config.ts index 3fa6253..d5f9920 100644 --- a/test/automation/base.config.ts +++ b/test/automation/base.config.ts @@ -1,4 +1,4 @@ -import { devices, ReporterDescription } from "@playwright/test"; +import { devices, type ReporterDescription } from "@playwright/test"; const baseConfig = { fullyParallel: true, diff --git a/test/automation/docs/CHANGELOG.md b/test/automation/docs/CHANGELOG.md index 6a53fe4..2d6f487 100644 --- a/test/automation/docs/CHANGELOG.md +++ b/test/automation/docs/CHANGELOG.md @@ -5,6 +5,10 @@ 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.1.3] - 2025-08-19 + +- move to import type for `ReporterDescription` + ## [0.1.2] - 2024-12-18 ### Fixed From bad0091a4ad8967a59a13430cf529e6dbd51b0f0 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:49:19 +0100 Subject: [PATCH 11/44] import playwright types explicitly --- examples/express/docs/CHANGELOG.md | 1 + examples/express/playwright.config.ts | 2 +- examples/next/docs/CHANGELOG.md | 4 ++++ examples/next/playwright.config.ts | 2 +- examples/serve/docs/CHANGELOG.md | 4 ++++ examples/serve/playwright.config.ts | 2 +- 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/express/docs/CHANGELOG.md b/examples/express/docs/CHANGELOG.md index 03a6019..cfcccb5 100644 --- a/examples/express/docs/CHANGELOG.md +++ b/examples/express/docs/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - removed "Vary" header from "animals" example, the page is meant to be un-cacheable, and the value was wrong in any case +- import types explicitly from `@playwright/test` after unexpected pipeline failure ## [0.2.5] - 2025-05-27 diff --git a/examples/express/playwright.config.ts b/examples/express/playwright.config.ts index 0288d5e..c0e2fbd 100644 --- a/examples/express/playwright.config.ts +++ b/examples/express/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, PlaywrightTestConfig } from "@playwright/test"; +import { defineConfig, type PlaywrightTestConfig } from "@playwright/test"; import baseConfig from "../../test/automation/base.config"; const config: PlaywrightTestConfig = { diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index 08af4aa..84cc6eb 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - update to take supply static `webpackNormalModule` corresponding to webpack plugin [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) +### Fixed + +- import types explicitly from `@playwright/test` after unexpected pipeline failure + ## [0.2.4] - 2025-05-27 ### Changed diff --git a/examples/next/playwright.config.ts b/examples/next/playwright.config.ts index 2f163c0..88d6b5e 100644 --- a/examples/next/playwright.config.ts +++ b/examples/next/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, PlaywrightTestConfig } from "@playwright/test"; +import { defineConfig, type PlaywrightTestConfig } from "@playwright/test"; import baseConfig from "../../test/automation/base.config"; const THREE_MINUTES = 3 * 60 * 1000; diff --git a/examples/serve/docs/CHANGELOG.md b/examples/serve/docs/CHANGELOG.md index 17cf9f2..62bba3b 100644 --- a/examples/serve/docs/CHANGELOG.md +++ b/examples/serve/docs/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - updated toggle handlers to take a `variantPathMap` corresponding to webpack [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) +### Fixed + +- import types explicitly from `@playwright/test` after unexpected pipeline failure + ## [0.2.6] - 2025-07-14 ### Changed diff --git a/examples/serve/playwright.config.ts b/examples/serve/playwright.config.ts index c0a196a..1b73718 100644 --- a/examples/serve/playwright.config.ts +++ b/examples/serve/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, PlaywrightTestConfig } from "@playwright/test"; +import { defineConfig, type PlaywrightTestConfig } from "@playwright/test"; import baseConfig from "../../test/automation/base.config"; const config: PlaywrightTestConfig = { From 334d8f937de786b78a5a5c7e42a95e9f5316dfd1 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:56:16 +0100 Subject: [PATCH 12/44] more type explicitness --- examples/next/docs/CHANGELOG.md | 2 +- .../app/fixtures/experiments/playwright.setExperimentHeaders.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index 84cc6eb..be1aa72 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- import types explicitly from `@playwright/test` after unexpected pipeline failure +- import types explicitly from `@playwright/test` & internally, after unexpected pipeline failure ## [0.2.4] - 2025-05-27 diff --git a/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts b/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts index 20e0c84..1111f8f 100644 --- a/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts +++ b/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts @@ -6,7 +6,7 @@ import { PlaywrightWorkerOptions } from "@playwright/test"; -import { Experiments, Decision } from "./experiments"; +import type { Experiments, Decision } from "./experiments"; const setExperimentHeaders = ( test: TestType< From 4eba157183d13e8582c178ec31995f7c57db41ad Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:10:36 +0100 Subject: [PATCH 13/44] more explicit type imports --- examples/next/src/app/fixtures/experiments/getExperiments.ts | 2 +- examples/next/src/app/fixtures/experiments/layout.tsx | 2 +- .../src/app/fixtures/experiments/playwright.locateInExample.ts | 2 +- .../app/fixtures/experiments/playwright.setExperimentHeaders.ts | 2 +- examples/next/src/app/layout.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/next/src/app/fixtures/experiments/getExperiments.ts b/examples/next/src/app/fixtures/experiments/getExperiments.ts index fc2f1ab..80450be 100644 --- a/examples/next/src/app/fixtures/experiments/getExperiments.ts +++ b/examples/next/src/app/fixtures/experiments/getExperiments.ts @@ -1,5 +1,5 @@ import { headers } from "next/headers"; -import { Experiments } from "./experiments"; +import type { Experiments } from "./experiments"; export default async function getExperiments(): Promise< Experiments | Record diff --git a/examples/next/src/app/fixtures/experiments/layout.tsx b/examples/next/src/app/fixtures/experiments/layout.tsx index 7b23ca7..0fcf7c2 100644 --- a/examples/next/src/app/fixtures/experiments/layout.tsx +++ b/examples/next/src/app/fixtures/experiments/layout.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import type { ReactNode } from "react"; import getExperiments from "./getExperiments"; import Example from "./example"; diff --git a/examples/next/src/app/fixtures/experiments/playwright.locateInExample.ts b/examples/next/src/app/fixtures/experiments/playwright.locateInExample.ts index 32224e8..970c9d2 100644 --- a/examples/next/src/app/fixtures/experiments/playwright.locateInExample.ts +++ b/examples/next/src/app/fixtures/experiments/playwright.locateInExample.ts @@ -1,4 +1,4 @@ -import { Page, Locator } from "@playwright/test"; +import type { Page, Locator } from "@playwright/test"; function locateWithinExample(page: Page, testId: string): Locator { return page.locator("#example").getByTestId(testId); diff --git a/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts b/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts index 1111f8f..4b16956 100644 --- a/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts +++ b/examples/next/src/app/fixtures/experiments/playwright.setExperimentHeaders.ts @@ -1,4 +1,4 @@ -import { +import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, diff --git a/examples/next/src/app/layout.tsx b/examples/next/src/app/layout.tsx index bcc6be7..0b642db 100644 --- a/examples/next/src/app/layout.tsx +++ b/examples/next/src/app/layout.tsx @@ -1,6 +1,6 @@ import { Didact_Gothic } from "next/font/google"; import styles from "./styles.module.css"; -import { ReactNode } from "react"; +import type { ReactNode } from "react"; const didactGothic = Didact_Gothic({ display: "swap", From 7c71a5a96e5fdd37285fc8a6b0b3958ce1945292 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:15:07 +0100 Subject: [PATCH 14/44] sigh... more types --- .../fixtures/experiments/1-varied-component/playwright.spec.ts | 2 +- .../fixtures/experiments/4-varied-variant/playwright.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts index 2350b62..e61d32d 100644 --- a/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts +++ b/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, ConsoleMessage } from "@playwright/test"; +import { test, expect, type ConsoleMessage } from "@playwright/test"; import setExperimentHeaders from "../playwright.setExperimentHeaders"; import locateWithinExample from "../playwright.locateInExample"; import getFixtureURL from "../playwright.getFixtureUrl"; diff --git a/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts index 7e02386..e4b4fc4 100644 --- a/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts +++ b/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, ConsoleMessage } from "@playwright/test"; +import { test, expect, type ConsoleMessage } from "@playwright/test"; import setExperimentHeaders from "../playwright.setExperimentHeaders"; import locateWithinExample from "../playwright.locateInExample"; import getFixtureURL from "../playwright.getFixtureUrl"; From 955080fc5334a9b3e680de53559a65e96bc66d22 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:42:50 +0100 Subject: [PATCH 15/44] version & changelog for test/automation --- test/automation/docs/CHANGELOG.md | 2 ++ test/automation/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/automation/docs/CHANGELOG.md b/test/automation/docs/CHANGELOG.md index 2d6f487..add0c91 100644 --- a/test/automation/docs/CHANGELOG.md +++ b/test/automation/docs/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.3] - 2025-08-19 +## Changed + - move to import type for `ReporterDescription` ## [0.1.2] - 2024-12-18 diff --git a/test/automation/package.json b/test/automation/package.json index 4798d47..70dd3cb 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-automation-tests", - "version": "0.1.2", + "version": "0.1.3", "main": "index.js", "keywords": [], "type": "module", From b4b0f42b09f19dc4ec67277e6fd8a114038f0740 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 21 Aug 2025 23:02:23 +0100 Subject: [PATCH 16/44] parallel folder example --- examples/express/README.md | 10 + examples/express/eslint.config.mjs | 30 +- examples/express/package.json | 10 +- examples/express/src/index.js | 3 + .../src/routes/animals/featuresStore.js | 2 +- .../express/src/routes/animals/middleware.js | 2 +- .../express/src/routes/config/Component.jsx | 2 +- .../div-style/Large/Component.jsx | 2 +- .../div-style/Small/Component.jsx | 2 +- .../src/routes/config/featuresStore.js | 2 +- examples/express/src/routes/config/router.js | 2 +- .../routes/parallel-folder-convention/App.tsx | 25 ++ .../parallel-folder-convention/README.md | 121 ++++++ .../feature1/components/BottomBox/index.tsx | 7 + .../TopBoxChild/TopBoxButton/index.module.css | 4 + .../feature1/components/TopBox/index.tsx | 11 + .../__variants__/feature1/constants/index.ts | 1 + .../components/TopBox/TopBoxChild/index.tsx | 17 + .../TopBox/TopBoxChild/useFreeAnimals.ts | 15 + .../__variants__/feature2/constants/index.ts | 1 + .../feature2/state/modules/animals/slice.ts | 16 + .../__variants__/feature3/constants/index.ts | 1 + .../feature3/state/modules/animals/slice.ts | 21 + .../__variants__/feature4/constants/index.ts | 1 + .../feature4/state/modules/animals/slice.ts | 16 + .../parallel-folder-convention/client.js | 4 + .../AnimalPen/index.module.css | 11 + .../component-library/AnimalPen/index.tsx | 9 + .../component-library/control1.tsx | 18 + .../component-library/control2.tsx | 18 + .../component-library/styles.module.css | 29 ++ .../component-library/variant1.tsx | 18 + .../component-library/variant2.tsx | 18 + .../components/Animal/index.tsx | 5 + .../components/BottomBox/index.tsx | 15 + .../components/BottomBox/useAnimals.ts | 6 + .../TopBoxChild/TopBoxButton/index.module.css | 6 + .../TopBox/TopBoxChild/TopBoxButton/index.tsx | 12 + .../TopBoxChild/TopBoxButton/useAddAnimal.ts | 11 + .../components/TopBox/TopBoxChild/index.tsx | 11 + .../components/TopBox/index.tsx | 11 + .../constants/index.ts | 1 + .../parallel-folder-convention/global.d.ts | 2 + .../parallel-folder-convention/router.tsx | 56 +++ .../state/modules/animals/slice.ts | 24 + .../state/modules/index.ts | 5 + .../parallel-folder-convention/state/store.ts | 12 + .../state/useAppDispatch.ts | 6 + .../state/useAppSelector.ts | 6 + .../parallel-folder-convention/styles.css | 54 +++ .../toggle-plumbing/constants.ts | 1 + .../toggle-plumbing/featuresStore/browser.ts | 18 + .../toggle-plumbing/featuresStore/index.ts | 9 + .../toggle-plumbing/featuresStore/server.ts | 5 + .../toggle-plumbing/pointCutConfig.js | 42 ++ .../toggle-plumbing/serialization.ts | 8 + .../toggle-points/objectProxyTogglePoint.ts | 25 ++ .../reactComponentTogglePoint.ts | 9 + .../toggle-points/reduxSliceTogglePoint.ts | 15 + .../toggle-plumbing/toggleHandler.ts | 11 + .../parallel-folder-convention/tsconfig.json | 26 ++ .../parallel-folder-convention/typings.d.ts | 1 + examples/express/webpack.config.js | 60 ++- package-lock.json | 411 +++++++++++++++--- 64 files changed, 1273 insertions(+), 60 deletions(-) create mode 100644 examples/express/src/routes/parallel-folder-convention/App.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/README.md create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/BottomBox/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/TopBoxChild/TopBoxButton/index.module.css create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature1/constants/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/useFreeAnimals.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature2/constants/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature3/constants/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature3/state/modules/animals/slice.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature4/constants/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature4/state/modules/animals/slice.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/client.js create mode 100644 examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.module.css create mode 100644 examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/component-library/control1.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/component-library/control2.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/component-library/styles.module.css create mode 100644 examples/express/src/routes/parallel-folder-convention/component-library/variant1.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/component-library/variant2.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/components/Animal/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/components/BottomBox/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/components/BottomBox/useAnimals.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css create mode 100644 examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/useAddAnimal.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/components/TopBox/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/constants/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/global.d.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/router.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/state/modules/animals/slice.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/state/modules/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/state/store.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/state/useAppDispatch.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/state/useAppSelector.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/styles.css create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/constants.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reactComponentTogglePoint.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggleHandler.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/tsconfig.json create mode 100644 examples/express/src/routes/parallel-folder-convention/typings.d.ts diff --git a/examples/express/README.md b/examples/express/README.md index ac9e6f6..311762e 100644 --- a/examples/express/README.md +++ b/examples/express/README.md @@ -9,3 +9,13 @@ Some example applications based on an [express](https://expressjs.com/) router 2. [config](./src/routes/config/README.md) This example shows the use of the [`react-pointcuts`](../../packages/react-pointcuts/docs/README.md), [`features`](../../packages/features/docs/README.md), [`ssr`](../../packages/ssr/docs/README.md) and [`webpack`](../../packages/webpack/docs/README.md) packages. + +3. [parallel-folder-convention](./src/routes/config/README.md) + + This example shows the use of the [`react-pointcuts`](../../packages/react-pointcuts/docs/README.md), [`features`](../../packages/features/docs/README.md), [`ssr`](../../packages/ssr/docs/README.md) and [`webpack`](../../packages/webpack/docs/README.md) packages. + + It has a bespoke filesystem convention, with parallel directory hierarchies containing arbitrary replacements and patches of various types of module, including: + - constants + - css + - react components + - [redux slices](https://redux.js.org/tutorials/essentials/part-2-app-structure#redux-slices) \ No newline at end of file diff --git a/examples/express/eslint.config.mjs b/examples/express/eslint.config.mjs index 27253ea..28aacd6 100644 --- a/examples/express/eslint.config.mjs +++ b/examples/express/eslint.config.mjs @@ -1,15 +1,43 @@ import examplesConfig from "../eslint.config.mjs"; import asosConfigReact from "../../peripheral/eslint-config-asosconfig/react.js"; import asosConfigServer from "../../peripheral/eslint-config-asosconfig/server.js"; +import parser from "@typescript-eslint/parser"; +import globals from "globals"; export default [ ...[...asosConfigReact, ...asosConfigServer].map((config) => ({ - files: ["**/*.js"], + files: ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"], ...config, settings: { + "import/resolver": { + typescript: { + alwaysTryTypes: true, + project: "./src/routes/parallel-folder-convention/tsconfig.json" + } + }, react: { version: "detect" } + }, + rules: { + ...config.rules, + "react/prop-types": "off", + "prettier/prettier": [ + "error", + { + trailingComma: "none", + endOfLine: "auto" + } + ] + }, + languageOptions: { + ...config.languageOptions, + parser, + globals: { + CLIENT: "readonly", + ...globals.browser, + ...globals.node + } } })), ...examplesConfig, diff --git a/examples/express/package.json b/examples/express/package.json index 4894627..a45185b 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -20,23 +20,29 @@ "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", + "@reduxjs/toolkit": "^2.8.2", "cross-env": "^7.0.3", "express": "^4.17.1", "http-status-codes": "^2.3.0", "react": ">=17", - "react-dom": ">=17" + "react-dom": ">=17", + "react-redux": "^9.2.0", + "valtio": "^2.1.5" }, "devDependencies": { "babel-loader": "^9.2.1", "css-loader": "^7.1.2", + "enhanced-tsconfig-paths-webpack-plugin": "^0.2.3", "mini-css-extract-plugin": "^2.9.2", "path-exists-cli": "^2.0.0", "prop-types": "^15.7.2", + "source-map-loader": "^5.0.0", "style-loader": "^4.0.0", + "ts-loader": "^9.5.2", "webpack": "^5.38.1", "webpack-cli": "^4.7.2", "webpack-node-externals": "^3.0.0" diff --git a/examples/express/src/index.js b/examples/express/src/index.js index 11fa87e..1594241 100644 --- a/examples/express/src/index.js +++ b/examples/express/src/index.js @@ -1,12 +1,14 @@ import express from "express"; import animalsRouter from "./routes/animals/router.js"; import configRouter from "./routes/config/router.js"; +import parallelFolderConventionRouter from "./routes/parallel-folder-convention/router.tsx"; const app = express(); const PORT = process.env.PORT; app.use("/animals", animalsRouter); app.use("/config", configRouter); +app.use("/parallel-folder-convention", parallelFolderConventionRouter); app.get("/", (_, response) => { response.send(` @@ -39,6 +41,7 @@ app.get("/", (_, response) => {

`); diff --git a/examples/express/src/routes/animals/featuresStore.js b/examples/express/src/routes/animals/featuresStore.js index e446d88..dc28cf0 100644 --- a/examples/express/src/routes/animals/featuresStore.js +++ b/examples/express/src/routes/animals/featuresStore.js @@ -1,4 +1,4 @@ -import { nodeRequestScopedStoreFactory as featuresStoreFactory } from "@asos/web-toggle-point-features"; +import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/nodeRequestScopedFeaturesStoreFactory"; const featuresStore = featuresStoreFactory(); diff --git a/examples/express/src/routes/animals/middleware.js b/examples/express/src/routes/animals/middleware.js index ea8e1b7..a772845 100644 --- a/examples/express/src/routes/animals/middleware.js +++ b/examples/express/src/routes/animals/middleware.js @@ -8,7 +8,7 @@ const contextMiddleware = (request, response, scopeCallBack) => { response.status(StatusCodes.BAD_REQUEST).end(); return; } - featuresStore.useValue({ value: { version }, scopeCallBack }); + featuresStore.setValue({ value: { version }, scopeCallBack }); }; export default contextMiddleware; diff --git a/examples/express/src/routes/config/Component.jsx b/examples/express/src/routes/config/Component.jsx index 982eeed..a9c318b 100644 --- a/examples/express/src/routes/config/Component.jsx +++ b/examples/express/src/routes/config/Component.jsx @@ -2,7 +2,7 @@ const Component = () => (
- I'm Medium + I'm Medium
); diff --git a/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx b/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx index 348e1ae..04a41e9 100644 --- a/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx +++ b/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx @@ -8,7 +8,7 @@ const Component = ({ backgroundColor }) => ( backgroundColor }} > - I'm Large + I 'm Large ); diff --git a/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx b/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx index 7921e50..ecc46e7 100644 --- a/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx +++ b/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx @@ -8,7 +8,7 @@ const Component = ({ backgroundColor }) => ( backgroundColor }} > - I'm Small + I'm Small ); diff --git a/examples/express/src/routes/config/featuresStore.js b/examples/express/src/routes/config/featuresStore.js index 89fc5d4..cd44351 100644 --- a/examples/express/src/routes/config/featuresStore.js +++ b/examples/express/src/routes/config/featuresStore.js @@ -1,4 +1,4 @@ -import { ssrBackedReactContextFeaturesStoreFactory as featuresStoreFactory } from "@asos/web-toggle-point-features"; +import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/ssrBackedReactContextFeaturesStoreFactory"; const featuresStore = featuresStoreFactory({ name: "config", diff --git a/examples/express/src/routes/config/router.js b/examples/express/src/routes/config/router.js index d12d087..f4b2d8d 100644 --- a/examples/express/src/routes/config/router.js +++ b/examples/express/src/routes/config/router.js @@ -13,7 +13,7 @@ router.get("/*", (req, res) => { + + + + + ); +} diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md new file mode 100644 index 0000000..6723820 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -0,0 +1,121 @@ +# Express "parallel folder convention" example + +This example shows the use of the [`react-pointcuts`](../../../../../packages/react-pointcuts/docs/README.md), [`features`](../../../../../packages/features/docs/README.md), [`ssr`](../../../../../packages/ssr/docs/README.md) and [`webpack`](../../../../../packages/webpack/docs/README.md) packages, as part of an [express](https://expressjs.com/) application. + +An inbound header named "feature" can take the following values: + +- baseline (or omitted) +- feature1 +- feature2 +- feature3 +- feature4 + +...which is used server side (via a "node request scoped features store") to generate appropriate server-rendered content. + +The chosen feature state is serialized to the browser using the `ssr` package, and loaded into a "global features store", using [`valtio`](https://github.com/pmndrs/valtio), for reactivity. + +To demonstrate the reactivity, a drop-down allows changing of the selected feature state. + +## Filesystem structure + +The base / control folder structure is thus: + +```bash +├── components +│ ├── Animal +│ │ └── index.tsx +│ ├── BottomBox +│ │ ├── index.tsx +│ │ └── useAnimals.ts +│ └── TopBox +│ ├── TopBoxChild +│ │ ├── TopBoxButton +│ │ │ ├── index.module.css +│ │ │ ├── index.tsx +│ │ │ └── useAddAnimal.ts +│ │ └── index.tsx +│ └── index.tsx +├── constants +│ └── index.ts +└── state + ├── modules + │ ├── animals + │ │ └── slice.ts + │ └── index.ts + └── store.ts +``` + +## Variations + +The features comprise the following: + +### _baseline_ + +The base experience. Clicking the dog dispatches a redux action adding a dog to the bottom box + +### _feature1_ + +Varied react components (`TopBox` & `BottomBox`), at various depths in the folder structure. A varied constant for the animal emoji (`constants/index.ts`) and varied css (background colour of the button) (`TopBoxButton/index.module.css`). + +```bash +├── components +│ ├── BottomBox +│ │ └── index.tsx +│ └── TopBox +│ ├── TopBoxChild +│ │ └── TopBoxButton +│ │ └── index.module.css +│ └── index.tsx +└── constants + └── index.ts +``` + +### _feature2_ + +Varied constant (`constants/index.ts`), react component (`TopBoxChild`) and redux slice (`slice.ts`) connecting an additional action creator (`useFreeAnimal.ts`) to "free" added animals (clears the state collection) + +```bash +├── components +│ └── TopBox +│ └── TopBoxChild +│ ├── index.tsx +│ └── useFreeAnimals.ts +└── constants + └── index.ts +``` + +### _feature3_ + +Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with modified reducer action that multiplies rabbits, when added + +```bash +├── constants +│ └── index.ts +└── state + └── modules + └── animals + └── slice.ts +``` + +### _feature4_ + +Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with replaced redux selector that carcinizes previously added animals + +```bash +├── constants +│ └── index.ts +└── state + └── modules + └── animals + └── slice.ts +``` + +## Explanation + +The webpack plugin is configured with a toggle handler that maps variants to controls based on a parallel root folder. This allows for any file to be replaced at any depth. The example shows both complete replacements, and augmentations (importing of the base, then modifying). + +To vary react components, the toggle point from the `react-pointcuts` package is used. + +To vary CSS files and constants, a toggle point is used that utilises an [object proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), to intercept property access. Despite css and constants not being innately reactive, as long as they are accessed from something that does update with change of state (i.e. react components), their properties will also update based on the new feature. + +To vary [`redux` "slices"](https://redux.js.org/tutorials/essentials/part-2-app-structure#redux-slices), the same proxy-based toggle point is used, but to ensure the store root reducer is re-initialised with change of state, it includes a subscribed side effect that calls [replaceReducer](https://redux.js.org/usage/code-splitting#using-replacereducer) from the [`@reduxjs/toolkit`](https://github.com/reduxjs/redux-toolkit) package. Since individual slices are statically imported (non-reactive), the side effect first flushes the [require cache](https://webpack.js.org/api/module-methods/#requirecache), to ensure a freshly-toggled slice is provided to the toggle point. \ No newline at end of file diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/BottomBox/index.tsx b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/BottomBox/index.tsx new file mode 100644 index 0000000..8a13b34 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/BottomBox/index.tsx @@ -0,0 +1,7 @@ +import type { JSX } from "react"; +import Variant2 from "@/component-library/variant2"; +import BottomBoxBase from "@/components/BottomBox"; + +const BottomBox = (): JSX.Element => BottomBoxBase({ BoxControl: Variant2 }); + +export default BottomBox; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/TopBoxChild/TopBoxButton/index.module.css b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/TopBoxChild/TopBoxButton/index.module.css new file mode 100644 index 0000000..d12939b --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/TopBoxChild/TopBoxButton/index.module.css @@ -0,0 +1,4 @@ +.button { + composes: button from "../../../../../../components/TopBox/TopBoxChild/TopBoxButton/index.module.css"; + background-color: wheat; +} \ No newline at end of file diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/index.tsx b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/index.tsx new file mode 100644 index 0000000..7f4c291 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/components/TopBox/index.tsx @@ -0,0 +1,11 @@ +import type { JSX } from "react"; +import Variant1 from "@/component-library/variant1"; +import TopBoxChild from "@/components/TopBox/TopBoxChild"; + +const TopBox = (): JSX.Element => ( + + + +); + +export default TopBox; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/constants/index.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/constants/index.ts new file mode 100644 index 0000000..20dcc7c --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature1/constants/index.ts @@ -0,0 +1 @@ +export default { ANIMAL: "🐱" as "🐶" | "🐱" }; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/index.tsx b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/index.tsx new file mode 100644 index 0000000..dd3fc6e --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/index.tsx @@ -0,0 +1,17 @@ +import type { JSX } from "react"; +import Control2 from "@/component-library/control2"; +import useFreeAnimals from "./useFreeAnimals"; +import TopBoxButton from "@/components/TopBox/TopBoxChild/TopBoxButton"; +import styles from "@/components/TopBox/TopBoxChild/TopBoxButton/index.module.css"; +import constants from "../../../constants/index"; + +const TopBoxChild = (): JSX.Element => ( + + + + +); + +export default TopBoxChild; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/useFreeAnimals.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/useFreeAnimals.ts new file mode 100644 index 0000000..4d4b781 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/components/TopBox/TopBoxChild/useFreeAnimals.ts @@ -0,0 +1,15 @@ +import slice from "@/state/modules/animals/slice"; +import useAppDispatch from "@/state/useAppDispatch"; +import type { UnknownAction } from "@reduxjs/toolkit"; + +const useFreeAnimals = () => { + const dispatch = useAppDispatch(); + return () => { + const action = ( + slice.actions as typeof slice.actions & { freedom: () => UnknownAction } + ).freedom(); + dispatch(action); + }; +}; + +export default useFreeAnimals; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/constants/index.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/constants/index.ts new file mode 100644 index 0000000..0744493 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/constants/index.ts @@ -0,0 +1 @@ +export default { ANIMAL: "🐹", FREEDOM: "⛓️‍💥" }; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts new file mode 100644 index 0000000..961fbed --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts @@ -0,0 +1,16 @@ +import originalSlice from "@/state/modules/animals/slice"; +import { createSlice } from "@reduxjs/toolkit"; + +const animalsSlice = createSlice({ + initialState: originalSlice.getInitialState(), + name: originalSlice.name, + reducers: { + ...originalSlice.caseReducers, + freedom: () => { + return []; + } + }, + selectors: originalSlice.getSelectors() +}); + +export default animalsSlice; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature3/constants/index.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature3/constants/index.ts new file mode 100644 index 0000000..60c9e64 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature3/constants/index.ts @@ -0,0 +1 @@ +export default { ANIMAL: "🐰" as "🐶" | "🐱" | "🐰" }; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature3/state/modules/animals/slice.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature3/state/modules/animals/slice.ts new file mode 100644 index 0000000..573c5dd --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature3/state/modules/animals/slice.ts @@ -0,0 +1,21 @@ +import originalSlice from "@/state/modules/animals/slice"; +import { createSlice } from "@reduxjs/toolkit"; + +const animalsSlice = createSlice({ + initialState: originalSlice.getInitialState(), + name: originalSlice.name, + reducers: { + add(state, action) { + const count = state.filter((animal) => animal === action.payload).length; + for (let i = 0; i < (count || 1); i++) { + originalSlice.reducer(state, { + ...action, + payload: action.payload + }); + } + } + }, + selectors: originalSlice.getSelectors() +}); + +export default animalsSlice; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature4/constants/index.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature4/constants/index.ts new file mode 100644 index 0000000..67fb4a0 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature4/constants/index.ts @@ -0,0 +1 @@ +export default { ANIMAL: "🦀" }; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature4/state/modules/animals/slice.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature4/state/modules/animals/slice.ts new file mode 100644 index 0000000..810fa7e --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature4/state/modules/animals/slice.ts @@ -0,0 +1,16 @@ +import { createSelector } from "@reduxjs/toolkit"; +import originalSlice from "@/state/modules/animals/slice"; +import constants from "@/constants/index"; + +const getAllAnimalsAsCrabs = createSelector( + [originalSlice.selectors.getAnimals], + (animals) => Array(animals.length).fill(constants.ANIMAL) +); + +export default { + ...originalSlice, + selectors: { + ...originalSlice.selectors, + getAnimals: getAllAnimalsAsCrabs + } +}; diff --git a/examples/express/src/routes/parallel-folder-convention/client.js b/examples/express/src/routes/parallel-folder-convention/client.js new file mode 100644 index 0000000..1d33c70 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/client.js @@ -0,0 +1,4 @@ +import { hydrateRoot } from "react-dom/client"; +import App from "./App.tsx"; + +hydrateRoot(document.body.firstChild, ); diff --git a/examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.module.css b/examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.module.css new file mode 100644 index 0000000..49e6a57 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.module.css @@ -0,0 +1,11 @@ +.animalPen { + padding: 1em; + margin: 1em; + border-radius: 1em; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + max-width: 8em; + border: 1px dashed black; + background: lightgreen; + overflow: auto; + max-height: 40vh; +} diff --git a/examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.tsx b/examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.tsx new file mode 100644 index 0000000..e83155a --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/component-library/AnimalPen/index.tsx @@ -0,0 +1,9 @@ +import styles from "./index.module.css"; + +const AnimalPen = ({ animals }) => { + return animals.length ? ( +
{animals}
+ ) : null; +}; + +export default AnimalPen; diff --git a/examples/express/src/routes/parallel-folder-convention/component-library/control1.tsx b/examples/express/src/routes/parallel-folder-convention/component-library/control1.tsx new file mode 100644 index 0000000..9e5ade3 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/component-library/control1.tsx @@ -0,0 +1,18 @@ +import React, { ReactNode } from "react"; +import styles from "./styles.module.css"; + +interface Control1Props { + children?: ReactNode; +} + +const Control1: React.FC = ({ children = null }) => ( +
+ Control 1
+ {children} +
+); + +export default Control1; diff --git a/examples/express/src/routes/parallel-folder-convention/component-library/control2.tsx b/examples/express/src/routes/parallel-folder-convention/component-library/control2.tsx new file mode 100644 index 0000000..01d0f02 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/component-library/control2.tsx @@ -0,0 +1,18 @@ +import React, { ReactNode } from "react"; +import styles from "./styles.module.css"; + +interface Control2Props { + children?: ReactNode; +} + +const Control2: React.FC = ({ children = null }) => ( +
+ Control 2
+ {children} +
+); + +export default Control2; diff --git a/examples/express/src/routes/parallel-folder-convention/component-library/styles.module.css b/examples/express/src/routes/parallel-folder-convention/component-library/styles.module.css new file mode 100644 index 0000000..f539d46 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/component-library/styles.module.css @@ -0,0 +1,29 @@ +.example { + border: 1px solid black; + border-radius: 5px; + min-width: 200px; + min-height: 200px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 20px; + margin: 20px; + text-align: center; + max-width: 280px; +} +.control { + background-color: lightblue; +} +.variant1 { + background-color: lightgreen; +} +.variant2 { + background-color: lightpink; +} +.routeList { + list-style-type: circle; +} +.routeList li:hover { + list-style-type: disc +} diff --git a/examples/express/src/routes/parallel-folder-convention/component-library/variant1.tsx b/examples/express/src/routes/parallel-folder-convention/component-library/variant1.tsx new file mode 100644 index 0000000..172c301 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/component-library/variant1.tsx @@ -0,0 +1,18 @@ +import React, { ReactNode } from "react"; +import styles from "./styles.module.css"; + +interface Variant1Props { + children?: ReactNode; +} + +const Variant1: React.FC = ({ children = null }) => ( +
+ Variant 1
+ {children} +
+); + +export default Variant1; diff --git a/examples/express/src/routes/parallel-folder-convention/component-library/variant2.tsx b/examples/express/src/routes/parallel-folder-convention/component-library/variant2.tsx new file mode 100644 index 0000000..aa03206 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/component-library/variant2.tsx @@ -0,0 +1,18 @@ +import React, { ReactNode } from "react"; +import styles from "./styles.module.css"; + +interface Variant2Props { + children?: ReactNode; +} + +const Variant2: React.FC = ({ children = null }) => ( +
+ Variant 2
+ {children} +
+); + +export default Variant2; diff --git a/examples/express/src/routes/parallel-folder-convention/components/Animal/index.tsx b/examples/express/src/routes/parallel-folder-convention/components/Animal/index.tsx new file mode 100644 index 0000000..5ccab21 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/Animal/index.tsx @@ -0,0 +1,5 @@ +import constants from "@/constants/index"; + +const animal = () => constants.ANIMAL; + +export default animal; diff --git a/examples/express/src/routes/parallel-folder-convention/components/BottomBox/index.tsx b/examples/express/src/routes/parallel-folder-convention/components/BottomBox/index.tsx new file mode 100644 index 0000000..8261752 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/BottomBox/index.tsx @@ -0,0 +1,15 @@ +import type { JSX } from "react"; +import Control2 from "@/component-library/control2"; +import AnimalPen from "@/component-library/AnimalPen"; +import useAnimals from "./useAnimals"; + +const BottomBox = ({ BoxControl = Control2 }): JSX.Element => { + const animals = useAnimals(); + return ( + + + + ); +}; + +export default BottomBox; diff --git a/examples/express/src/routes/parallel-folder-convention/components/BottomBox/useAnimals.ts b/examples/express/src/routes/parallel-folder-convention/components/BottomBox/useAnimals.ts new file mode 100644 index 0000000..757f4d8 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/BottomBox/useAnimals.ts @@ -0,0 +1,6 @@ +import useAppSelector from "@/state/useAppSelector"; +import slice from "@/state/modules/animals/slice"; + +const useAnimals = () => useAppSelector(slice.selectors.getAnimals); + +export default useAnimals; diff --git a/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css new file mode 100644 index 0000000..3bf108e --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css @@ -0,0 +1,6 @@ +.button { + margin: auto; + border: 1px solid black; + border-radius: 20%; + font-size: xxx-large; +} \ No newline at end of file diff --git a/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.tsx b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.tsx new file mode 100644 index 0000000..71b0989 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.tsx @@ -0,0 +1,12 @@ +import type { JSX } from "react"; +import useAddAnimal from "./useAddAnimal"; +import Animal from "@/components/Animal"; +import styles from "./index.module.css"; + +const TopBoxButton = (): JSX.Element => ( + +); + +export default TopBoxButton; diff --git a/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/useAddAnimal.ts b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/useAddAnimal.ts new file mode 100644 index 0000000..646aa80 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/useAddAnimal.ts @@ -0,0 +1,11 @@ +import slice from "@/state/modules/animals/slice"; +import useAppDispatch from "@/state/useAppDispatch"; +import constants from "@/constants/index"; + +const useAddAnimal = () => { + const action = slice.actions.add(constants.ANIMAL); + const dispatch = useAppDispatch(); + return () => dispatch(action); +}; + +export default useAddAnimal; diff --git a/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/index.tsx b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/index.tsx new file mode 100644 index 0000000..3182545 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/index.tsx @@ -0,0 +1,11 @@ +import type { JSX } from "react"; +import Control2 from "@/component-library/control2"; +import TopBoxButton from "@/components/TopBox/TopBoxChild/TopBoxButton"; + +const TopBoxChild = (): JSX.Element => ( + + + +); + +export default TopBoxChild; diff --git a/examples/express/src/routes/parallel-folder-convention/components/TopBox/index.tsx b/examples/express/src/routes/parallel-folder-convention/components/TopBox/index.tsx new file mode 100644 index 0000000..179ad6e --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/components/TopBox/index.tsx @@ -0,0 +1,11 @@ +import type { JSX } from "react"; +import Control1 from "@/component-library/control1"; +import TopBoxChild from "./TopBoxChild"; + +const TopBox = (): JSX.Element => ( + + + +); + +export default TopBox; diff --git a/examples/express/src/routes/parallel-folder-convention/constants/index.ts b/examples/express/src/routes/parallel-folder-convention/constants/index.ts new file mode 100644 index 0000000..8433764 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/constants/index.ts @@ -0,0 +1 @@ +export default { ANIMAL: "🐶" as "🐶" }; diff --git a/examples/express/src/routes/parallel-folder-convention/global.d.ts b/examples/express/src/routes/parallel-folder-convention/global.d.ts new file mode 100644 index 0000000..457b234 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/global.d.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-unused-vars +declare const CLIENT: boolean; diff --git a/examples/express/src/routes/parallel-folder-convention/router.tsx b/examples/express/src/routes/parallel-folder-convention/router.tsx new file mode 100644 index 0000000..9e9c80e --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/router.tsx @@ -0,0 +1,56 @@ +import { Router, static as assetsFolder } from "express"; +import { renderToPipeableStream } from "react-dom/server"; +import { getScriptReactElement } from "./toggle-plumbing/serialization"; +import { setValue, getFeatures } from "./toggle-plumbing/featuresStore"; +import App from "./App"; + +const router = new Router(); +router.use(assetsFolder("public")); +router.use("/*", (request, _, scopeCallBack) => { + setValue({ + value: { selection: request.headers.feature || "baseline" }, + scopeCallBack + }); +}); +router.get("/*", (_, res) => { + const { pipe } = renderToPipeableStream( + + + + + + + + {getScriptReactElement({ content: getFeatures() })} + + +
+ +
+ + , + { + bootstrapScripts: [ + "/parallel-folder-convention/parallel-folder-convention.js" + ], + onShellReady() { + res.statusCode = 200; + res.setHeader("Content-type", "text/html"); + pipe(res); + }, + onShellError() { + res.statusCode = 500; + res.send("

Loading...

"); + } + } + ); +}); + +export default router; diff --git a/examples/express/src/routes/parallel-folder-convention/state/modules/animals/slice.ts b/examples/express/src/routes/parallel-folder-convention/state/modules/animals/slice.ts new file mode 100644 index 0000000..156297b --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/state/modules/animals/slice.ts @@ -0,0 +1,24 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import constants from "../../../constants"; + +type Animals = typeof constants.ANIMAL; + +interface AnimalsState extends Array {} + +const initialState = [] satisfies AnimalsState as AnimalsState; + +const animalsSlice = createSlice({ + name: "animals", + initialState, + reducers: { + add(state, action: PayloadAction) { + state.push(action.payload); + } + }, + selectors: { + getAnimals: (state: AnimalsState) => state + } +}); + +export default animalsSlice; diff --git a/examples/express/src/routes/parallel-folder-convention/state/modules/index.ts b/examples/express/src/routes/parallel-folder-convention/state/modules/index.ts new file mode 100644 index 0000000..f21d3fc --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/state/modules/index.ts @@ -0,0 +1,5 @@ +import animalsSlice from "./animals/slice"; + +export default { + animals: animalsSlice.reducer +}; diff --git a/examples/express/src/routes/parallel-folder-convention/state/store.ts b/examples/express/src/routes/parallel-folder-convention/state/store.ts new file mode 100644 index 0000000..af10ccf --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/state/store.ts @@ -0,0 +1,12 @@ +import { configureStore } from "@reduxjs/toolkit"; +import reducerMap from "./modules/index"; + +const store = configureStore({ + reducer: reducerMap +}); + +export type AppStore = typeof store; +export type RootState = ReturnType; +export type AppDispatch = AppStore["dispatch"]; + +export default store; diff --git a/examples/express/src/routes/parallel-folder-convention/state/useAppDispatch.ts b/examples/express/src/routes/parallel-folder-convention/state/useAppDispatch.ts new file mode 100644 index 0000000..80abae5 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/state/useAppDispatch.ts @@ -0,0 +1,6 @@ +import { useDispatch } from "react-redux"; +import type { AppDispatch } from "./store"; + +const useAppDispatch = useDispatch.withTypes(); + +export default useAppDispatch; diff --git a/examples/express/src/routes/parallel-folder-convention/state/useAppSelector.ts b/examples/express/src/routes/parallel-folder-convention/state/useAppSelector.ts new file mode 100644 index 0000000..331e88d --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/state/useAppSelector.ts @@ -0,0 +1,6 @@ +import { useSelector } from "react-redux"; +import type { RootState } from "./store"; + +const useAppSelector = useSelector.withTypes(); + +export default useAppSelector; diff --git a/examples/express/src/routes/parallel-folder-convention/styles.css b/examples/express/src/routes/parallel-folder-convention/styles.css new file mode 100644 index 0000000..7685138 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/styles.css @@ -0,0 +1,54 @@ +body { + font-family: "Didact Gothic", sans-serif; + font-weight: 400; + font-style: normal; + height: 100vh; + display: flex; + margin: 0; + flex-direction: column; + align-items: center; + justify-content: center; + + > div { + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + padding: 2em; + flex-basis: content; + + & > div { + display: flex; + flex: 1 1 auto; + justify-content: center; + flex-direction: column; + padding: 3em; + } + } +} + +select, +::picker(select) { + appearance: base-select; + backdrop-filter: saturate(0.3); +} +::picker(select) { + opacity: 0; + transition: all 0.4s allow-discrete; +} +select::picker-icon { + margin: auto; +} +option::checkmark { + order: 1; + margin-left: auto; + content: "☑️"; +} +::picker(select):popover-open { + opacity: 1; +} +@starting-style { + ::picker(select):popover-open { + opacity: 0; + } +} diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/constants.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/constants.ts new file mode 100644 index 0000000..fca1a93 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/constants.ts @@ -0,0 +1 @@ +export const FEATURE_KEY = "feature"; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts new file mode 100644 index 0000000..5feee19 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts @@ -0,0 +1,18 @@ +import { proxy, useSnapshot } from "valtio"; +import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/globalFeaturesStoreFactory"; +import { getJSONFromScript } from "../serialization"; + +const { setValue: storeSetValue, getFeatures } = featuresStoreFactory(); + +storeSetValue({ value: proxy({}) }); +export const setValue = (input) => { + storeSetValue({ + value: Object.assign(getFeatures(), input) + }); +}; + +export { getFeatures }; +export const useFeatures = () => ({ feature: useSnapshot(getFeatures()) }); + +const { selection } = getJSONFromScript(); +setValue({ selection }); diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/index.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/index.ts new file mode 100644 index 0000000..01c3019 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/index.ts @@ -0,0 +1,9 @@ +let getFeatures, setValue, useFeatures; + +if (CLIENT) { + ({ getFeatures, setValue, useFeatures } = require("./browser.ts")); +} else { + ({ getFeatures, setValue, useFeatures } = require("./server.ts")); +} + +export { getFeatures, setValue, useFeatures }; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts new file mode 100644 index 0000000..3f15b50 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts @@ -0,0 +1,5 @@ +import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/nodeRequestScopedFeaturesStoreFactory"; +const { setValue, getFeatures } = featuresStoreFactory(); + +export { getFeatures, setValue }; +export const useFeatures = () => ({ feature: getFeatures() }); diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js new file mode 100644 index 0000000..626f454 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js @@ -0,0 +1,42 @@ +import { win32, posix } from "path"; + +const toggleHandler = + "/src/routes/parallel-folder-convention/toggle-plumbing/toggleHandler"; +const joinPointResolver = (path) => + path.replaceAll(win32.sep, posix.sep).replace(/__variants__\/[^/]+\//, ""); + +const common = { + toggleHandler, + joinPointResolver +}; + +export default [ + { + name: "react components", + togglePointModule: + "/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reactComponentTogglePoint", + variantGlobs: [ + "./src/routes/parallel-folder-convention/__variants__/*/components/**/!(*.spec).tsx" + ], + ...common + }, + { + name: "css modules & constants", + togglePointModule: + "/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint", + variantGlobs: [ + "./src/routes/parallel-folder-convention/__variants__/*/components/**/*.css", + "./src/routes/parallel-folder-convention/__variants__/*/constants/**/*.ts" + ], + ...common + }, + { + name: "redux slices", + togglePointModule: + "/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint", + variantGlobs: [ + "./src/routes/parallel-folder-convention/__variants__/*/state/modules/*/slice.ts" + ], + ...common + } +]; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts new file mode 100644 index 0000000..5f754a1 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts @@ -0,0 +1,8 @@ +import { serializationFactory } from "@asos/web-toggle-point-ssr"; + +const { getScriptReactElement, getJSONFromScript } = serializationFactory({ + id: "features", + logWarning: console.warn +}); + +export { getScriptReactElement, getJSONFromScript }; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts new file mode 100644 index 0000000..256960a --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts @@ -0,0 +1,25 @@ +import { getFeatures } from "../featuresStore"; +import { FEATURE_KEY } from "../constants"; + +const getRelevantModule = (joinPoint, featuresMap) => { + const activeFeatures = featuresMap.get(FEATURE_KEY); + try { + const { selection } = getFeatures(); + + const variant = activeFeatures.get(selection); + return variant ?? joinPoint; + } catch { + return joinPoint; + } +}; + +const togglePoint = (joinPoint, featuresMap) => { + return new Proxy(joinPoint.default, { + get(_, ...rest) { + const newTarget = getRelevantModule(joinPoint, featuresMap); + return Reflect.get(newTarget.default, ...rest); + } + }); +}; + +export default togglePoint; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reactComponentTogglePoint.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reactComponentTogglePoint.ts new file mode 100644 index 0000000..c75c0d6 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reactComponentTogglePoint.ts @@ -0,0 +1,9 @@ +import { withTogglePointFactory } from "@asos/web-toggle-point-react-pointcuts"; +import { useFeatures as getActiveFeatures } from "../featuresStore"; + +const togglePoint = withTogglePointFactory({ + getActiveFeatures, + variantKey: "selection" +}); + +export default togglePoint; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts new file mode 100644 index 0000000..2bfd06f --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts @@ -0,0 +1,15 @@ +import objectProxyTogglePoint from "./objectProxyTogglePoint"; +import { combineReducers } from "@reduxjs/toolkit"; +import { getFeatures } from "../featuresStore"; +import { subscribe } from "valtio"; + +if (CLIENT) { + subscribe(getFeatures(), () => { + delete require.cache[require.resolve("@/state/modules")]; + const { default: newReducerMap } = require("@/state/modules"); + const { default: store } = require("@/state/store"); + store.replaceReducer(combineReducers(newReducerMap)); + }); +} + +export default objectProxyTogglePoint; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggleHandler.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggleHandler.ts new file mode 100644 index 0000000..548ab19 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggleHandler.ts @@ -0,0 +1,11 @@ +import { FEATURE_KEY } from "./constants"; + +export default ({ togglePoint, joinPoint, variantPathMap }) => { + const variantsMap = new Map(); + for (const key of variantPathMap.keys()) { + const [, feature] = key.match(/\/__variants__\/(.+?)\//); + variantsMap.set(feature, variantPathMap.get(key)); + } + const featuresMap = new Map([[FEATURE_KEY, variantsMap]]); + return togglePoint(joinPoint, featuresMap); +}; diff --git a/examples/express/src/routes/parallel-folder-convention/tsconfig.json b/examples/express/src/routes/parallel-folder-convention/tsconfig.json new file mode 100644 index 0000000..837544f --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": ".", + "paths": { + "@/component-library/*": ["./component-library/*"], + "@/components/*": ["./components/*"], + "@/state/*": ["./state/*"], + "@/constants/*": ["./constants/*"] + }, + "target": "es2018", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "sourceMap": true + }, + "include": ["**/*.ts", "**/*.tsx", "toggle-plumbing/pointCutConfig.js"], + "exclude": ["node_modules"] +} diff --git a/examples/express/src/routes/parallel-folder-convention/typings.d.ts b/examples/express/src/routes/parallel-folder-convention/typings.d.ts new file mode 100644 index 0000000..1eabbb4 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/typings.d.ts @@ -0,0 +1 @@ +declare module "*.module.css"; diff --git a/examples/express/webpack.config.js b/examples/express/webpack.config.js index 73d72f4..ff20d6e 100644 --- a/examples/express/webpack.config.js +++ b/examples/express/webpack.config.js @@ -3,6 +3,9 @@ import externals from "webpack-node-externals"; import { TogglePointInjection } from "@asos/web-toggle-point-webpack/plugins"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import { fileURLToPath } from "url"; +import parallelFolderConventionPointCutConfig from "./src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js"; +import { EnhancedTsconfigWebpackPlugin } from "enhanced-tsconfig-paths-webpack-plugin"; +import webpack from "webpack"; const configPointCutConfig = { name: "configuration variants", @@ -12,6 +15,7 @@ const configPointCutConfig = { const common = { mode: "production", + devtool: "source-map", module: { rules: [ { @@ -23,9 +27,39 @@ const common = { }, { test: /\.css$/, - use: [MiniCssExtractPlugin.loader, "css-loader"] + use: [ + MiniCssExtractPlugin.loader, + { + loader: "css-loader", + options: { + modules: { + namedExport: false + } + } + } + ] + }, + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/ + }, + { + test: /\.js$/, + enforce: "pre", + use: ["source-map-loader"] } ] + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + plugins: [ + new EnhancedTsconfigWebpackPlugin({ + tsconfigPaths: { + extensions: [".ts", ".tsx", ".css"] + } + }) + ] } }; @@ -42,10 +76,14 @@ const config = [ externals: [externals()], ...common, plugins: [ + new webpack.DefinePlugin({ + CLIENT: false + }), new MiniCssExtractPlugin(), new TogglePointInjection({ pointCuts: [ configPointCutConfig, + ...parallelFolderConventionPointCutConfig, { name: "animal apis by version", variantGlobs: [ @@ -75,6 +113,26 @@ const config = [ new TogglePointInjection({ pointCuts: [configPointCutConfig] }) ], ...common + }, + { + entry: "./src/routes/parallel-folder-convention/client.js", + target: "web", + output: { + path: resolve(dirname(fileURLToPath(import.meta.url)), "public"), + filename: "parallel-folder-convention.js" + }, + plugins: [ + new webpack.DefinePlugin({ + CLIENT: true + }), + new MiniCssExtractPlugin({ + filename: "parallel-folder-convention.css" + }), + new TogglePointInjection({ + pointCuts: parallelFolderConventionPointCutConfig + }) + ], + ...common } ]; diff --git a/package-lock.json b/package-lock.json index 3929385..51778b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,19 +63,25 @@ "@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", + "@reduxjs/toolkit": "^2.8.2", "cross-env": "^7.0.3", "express": "^4.17.1", "http-status-codes": "^2.3.0", "react": ">=17", - "react-dom": ">=17" + "react-dom": ">=17", + "react-redux": "^9.2.0", + "valtio": "^2.1.5" }, "devDependencies": { "babel-loader": "^9.2.1", "css-loader": "^7.1.2", + "enhanced-tsconfig-paths-webpack-plugin": "^0.2.3", "mini-css-extract-plugin": "^2.9.2", "path-exists-cli": "^2.0.0", "prop-types": "^15.7.2", + "source-map-loader": "^5.0.0", "style-loader": "^4.0.0", + "ts-loader": "^9.5.2", "webpack": "^5.38.1", "webpack-cli": "^4.7.2", "webpack-node-externals": "^3.0.0" @@ -1771,7 +1777,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -4053,6 +4061,32 @@ "node": ">=18" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-babel": { "version": "6.0.4", "dev": true, @@ -4273,6 +4307,18 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin-js": { "version": "2.12.1", "license": "MIT", @@ -4658,6 +4704,12 @@ "version": "3.0.3", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/webpack-env": { "version": "1.18.5", "dev": true, @@ -4677,19 +4729,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", "devOptional": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/type-utils": "8.18.1", - "@typescript-eslint/utils": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4699,20 +4753,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.40.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", "devOptional": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4" }, "engines": { @@ -4724,33 +4790,75 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1" + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", "devOptional": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/utils": "8.18.1", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4761,11 +4869,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4776,17 +4886,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4796,11 +4910,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4808,6 +4924,8 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4820,7 +4938,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4830,13 +4950,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4847,15 +4969,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.1", + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4866,7 +4990,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7096,7 +7222,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7468,6 +7596,16 @@ "node": ">=10.13.0" } }, + "node_modules/enhanced-tsconfig-paths-webpack-plugin": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/enhanced-tsconfig-paths-webpack-plugin/-/enhanced-tsconfig-paths-webpack-plugin-0.2.3.tgz", + "integrity": "sha512-r1rVm5O8+VwY2ZEU2JU0CG5UW+k5A2+sQSnixapmJFaDxYGEvjBXPLbehJGF/AcTwNvpEDpVoKTKpdLoKzNezg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tsconfig-paths": "^3.10.1" + } + }, "node_modules/entities": { "version": "4.5.0", "license": "BSD-2-Clause", @@ -9421,7 +9559,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.8.1", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9895,6 +10035,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "license": "MIT", @@ -16130,6 +16280,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", + "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "dev": true, @@ -16300,6 +16456,29 @@ "version": "2.0.2", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/read-package-json-fast": { "version": "3.0.2", "dev": true, @@ -16534,6 +16713,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.9", "license": "MIT", @@ -16764,6 +16958,12 @@ "lodash": "^4.17.21" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "license": "MIT", @@ -17685,6 +17885,40 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "devOptional": true, @@ -18376,13 +18610,59 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, "node_modules/tsconfig-paths": { @@ -18932,6 +19212,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -19008,6 +19297,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/valtio": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.5.tgz", + "integrity": "sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==", + "license": "MIT", + "dependencies": { + "proxy-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/vary": { "version": "1.1.2", "license": "MIT", @@ -19916,7 +20229,7 @@ }, "packages/webpack": { "name": "@asos/web-toggle-point-webpack", - "version": "0.9.0", + "version": "0.9.1", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", From 5a500970ee85f0a4438ae56a7324d7782b04ede6 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:02:24 +0100 Subject: [PATCH 17/44] changelog --- examples/express/docs/CHANGELOG.md | 6 ++++++ examples/express/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/express/docs/CHANGELOG.md b/examples/express/docs/CHANGELOG.md index cfcccb5..e48b005 100644 --- a/examples/express/docs/CHANGELOG.md +++ b/examples/express/docs/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] - 2025-08-21 + +### Changed + +- added "parallel folder convention" example + ## [0.2.6] - 2025-07-29 ### Fixed diff --git a/examples/express/package.json b/examples/express/package.json index a45185b..e2271c3 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-express-example", - "version": "0.2.6", + "version": "0.3.0", "type": "module", "engines": { "node": ">=20.6.0" From 0039376da0552ce03584578885ee5b82d5ecd677 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:49:15 +0100 Subject: [PATCH 18/44] fix path in readme --- examples/express/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/express/README.md b/examples/express/README.md index 311762e..b90634f 100644 --- a/examples/express/README.md +++ b/examples/express/README.md @@ -10,7 +10,7 @@ Some example applications based on an [express](https://expressjs.com/) router This example shows the use of the [`react-pointcuts`](../../packages/react-pointcuts/docs/README.md), [`features`](../../packages/features/docs/README.md), [`ssr`](../../packages/ssr/docs/README.md) and [`webpack`](../../packages/webpack/docs/README.md) packages. -3. [parallel-folder-convention](./src/routes/config/README.md) +3. [parallel-folder-convention](./src/routes/parallel-folder-convention/README.md) This example shows the use of the [`react-pointcuts`](../../packages/react-pointcuts/docs/README.md), [`features`](../../packages/features/docs/README.md), [`ssr`](../../packages/ssr/docs/README.md) and [`webpack`](../../packages/webpack/docs/README.md) packages. From 410a3e3f24a4fd1a866303e11fef34f89e5c09d9 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:52:25 +0100 Subject: [PATCH 19/44] readme updates --- .../express/src/routes/parallel-folder-convention/README.md | 4 ++-- packages/features/docs/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md index 6723820..3d16756 100644 --- a/examples/express/src/routes/parallel-folder-convention/README.md +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -10,9 +10,9 @@ An inbound header named "feature" can take the following values: - feature3 - feature4 -...which is used server side (via a "node request scoped features store") to generate appropriate server-rendered content. +...which is used server side (via a ["node request scoped features store"](../../../../../packages/features/docs/README.md#noderequestscopedfeaturesstorefactory)) to generate appropriate server-rendered content. -The chosen feature state is serialized to the browser using the `ssr` package, and loaded into a "global features store", using [`valtio`](https://github.com/pmndrs/valtio), for reactivity. +The chosen feature state is serialized to the browser using the `ssr` package, and loaded into a ["global features store"](../../../../../packages/features/docs/README.md#globalfeaturesstorefactory), using [`valtio`](https://github.com/pmndrs/valtio), for reactivity. To demonstrate the reactivity, a drop-down allows changing of the selected feature state. diff --git a/packages/features/docs/README.md b/packages/features/docs/README.md index 4290324..526606f 100644 --- a/packages/features/docs/README.md +++ b/packages/features/docs/README.md @@ -47,7 +47,7 @@ const getActiveFeatures = useSnapshot.bind(undefined, value); // passed to `with ``` ...which will then re-render consuming components based on the parts of the toggle state they are reliant on. -### `nodeRequestScopedStoreFactory` +### `nodeRequestScopedFeaturesStoreFactory` A "request scoped" features store factory, for use in [Node](https://nodejs.org/). From 05bcc4f0d3eeef12cc3b0da0a3df913865e84eeb Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:18:41 +0100 Subject: [PATCH 20/44] update for change in initial state --- .../express/src/routes/parallel-folder-convention/README.md | 2 +- .../__variants__/feature2/state/modules/animals/slice.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md index 3d16756..b01c91f 100644 --- a/examples/express/src/routes/parallel-folder-convention/README.md +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -72,7 +72,7 @@ Varied react components (`TopBox` & `BottomBox`), at various depths in the folde ### _feature2_ -Varied constant (`constants/index.ts`), react component (`TopBoxChild`) and redux slice (`slice.ts`) connecting an additional action creator (`useFreeAnimal.ts`) to "free" added animals (clears the state collection) +Varied constant (`constants/index.ts`), react component (`TopBoxChild`) and redux slice (`slice.ts`) connecting an additional action creator (`useFreeAnimal.ts`) to "free" added animals (clears the state collection). Has an alternate "initial state" containing two hamsters, activated during server rendering by a `feature` header containing `feature2`. ```bash ├── components diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts index 961fbed..5f92a8b 100644 --- a/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature2/state/modules/animals/slice.ts @@ -1,8 +1,9 @@ import originalSlice from "@/state/modules/animals/slice"; import { createSlice } from "@reduxjs/toolkit"; +import constant from "../../../constants"; const animalsSlice = createSlice({ - initialState: originalSlice.getInitialState(), + initialState: [constant.ANIMAL, constant.ANIMAL], name: originalSlice.name, reducers: { ...originalSlice.caseReducers, From 00e7a277d5dd10bc0083e480953a3b2e5717c588 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:23:30 +0100 Subject: [PATCH 21/44] remove require.cache hackery move reducer replacement to a provider, rather than a toggle side-effect --- .../routes/parallel-folder-convention/App.tsx | 7 +++---- .../parallel-folder-convention/README.md | 5 +++-- .../StateProvider.tsx | 20 +++++++++++++++++++ .../state/modules/index.ts | 6 ++++-- .../parallel-folder-convention/state/store.ts | 16 +++++++++------ .../toggle-plumbing/pointCutConfig.js | 12 ++--------- .../toggle-points/objectProxyTogglePoint.ts | 11 +++------- .../toggle-points/reduxSliceTogglePoint.ts | 15 -------------- 8 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 examples/express/src/routes/parallel-folder-convention/StateProvider.tsx delete mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts diff --git a/examples/express/src/routes/parallel-folder-convention/App.tsx b/examples/express/src/routes/parallel-folder-convention/App.tsx index 97164e6..339d69b 100644 --- a/examples/express/src/routes/parallel-folder-convention/App.tsx +++ b/examples/express/src/routes/parallel-folder-convention/App.tsx @@ -1,13 +1,12 @@ import TopBox from "./components/TopBox"; import BottomBox from "./components/BottomBox"; -import { Provider } from "react-redux"; -import store from "./state/store"; +import StateProvider from "./StateProvider"; import { getFeatures, setValue } from "./toggle-plumbing/featuresStore"; import "./styles.css"; export default function App() { return ( - + - + ); } diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md index b01c91f..88bd08a 100644 --- a/examples/express/src/routes/parallel-folder-convention/README.md +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -116,6 +116,7 @@ The webpack plugin is configured with a toggle handler that maps variants to con To vary react components, the toggle point from the `react-pointcuts` package is used. -To vary CSS files and constants, a toggle point is used that utilises an [object proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), to intercept property access. Despite css and constants not being innately reactive, as long as they are accessed from something that does update with change of state (i.e. react components), their properties will also update based on the new feature. +To vary CSS files, constants, and [`redux` "slices"](https://redux.js.org/tutorials/essentials/part-2-app-structure#redux-slices), a toggle point is used that utilises an [object proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), to intercept property access. Despite objects not being innately reactive, as long as they are accessed from something that does update with change of state (i.e. react components), their properties will also update based on the new feature. + +To ensure that the redux store is reactive to feature state, a `StateProvider` react component is used to host the [react-redux provider](https://react-redux.js.org/api/provider), that subscribes to the valtio state, and calls [replaceReducer](https://redux.js.org/usage/code-splitting#using-replacereducer) from the [`@reduxjs/toolkit`](https://github.com/reduxjs/redux-toolkit) whenever the feature state changes. This ensures a new redux store (but retaining current state) is made available to the react components. Care should be taken to ensure the existing state is compatible with any updated selectors etc. -To vary [`redux` "slices"](https://redux.js.org/tutorials/essentials/part-2-app-structure#redux-slices), the same proxy-based toggle point is used, but to ensure the store root reducer is re-initialised with change of state, it includes a subscribed side effect that calls [replaceReducer](https://redux.js.org/usage/code-splitting#using-replacereducer) from the [`@reduxjs/toolkit`](https://github.com/reduxjs/redux-toolkit) package. Since individual slices are statically imported (non-reactive), the side effect first flushes the [require cache](https://webpack.js.org/api/module-methods/#requirecache), to ensure a freshly-toggled slice is provided to the toggle point. \ No newline at end of file diff --git a/examples/express/src/routes/parallel-folder-convention/StateProvider.tsx b/examples/express/src/routes/parallel-folder-convention/StateProvider.tsx new file mode 100644 index 0000000..977db5d --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/StateProvider.tsx @@ -0,0 +1,20 @@ +import { Provider } from "react-redux"; +import { combineReducers } from "@reduxjs/toolkit"; +import createStore from "@/state/store"; +import { getFeatures } from "./toggle-plumbing/featuresStore"; +import getReducerMap from "@/state/modules"; +import { subscribe } from "valtio"; + +const StateProvider = ({ children }) => { + const store = createStore(); + + if (CLIENT) { + subscribe(getFeatures(), () => { + store.replaceReducer(combineReducers(getReducerMap())); + }); + } + + return {children}; +}; + +export default StateProvider; diff --git a/examples/express/src/routes/parallel-folder-convention/state/modules/index.ts b/examples/express/src/routes/parallel-folder-convention/state/modules/index.ts index f21d3fc..e149a09 100644 --- a/examples/express/src/routes/parallel-folder-convention/state/modules/index.ts +++ b/examples/express/src/routes/parallel-folder-convention/state/modules/index.ts @@ -1,5 +1,7 @@ import animalsSlice from "./animals/slice"; -export default { +const getReducerMap = () => ({ animals: animalsSlice.reducer -}; +}); + +export default getReducerMap; diff --git a/examples/express/src/routes/parallel-folder-convention/state/store.ts b/examples/express/src/routes/parallel-folder-convention/state/store.ts index af10ccf..e6fa8b2 100644 --- a/examples/express/src/routes/parallel-folder-convention/state/store.ts +++ b/examples/express/src/routes/parallel-folder-convention/state/store.ts @@ -1,12 +1,16 @@ import { configureStore } from "@reduxjs/toolkit"; -import reducerMap from "./modules/index"; +import getReducerMap from "./modules/index"; -const store = configureStore({ - reducer: reducerMap -}); +const createStore = () => { + const store = configureStore({ + reducer: getReducerMap() + }); + return store; +}; + +export type AppStore = ReturnType; -export type AppStore = typeof store; export type RootState = ReturnType; export type AppDispatch = AppStore["dispatch"]; -export default store; +export default createStore; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js index 626f454..eb05638 100644 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js @@ -21,20 +21,12 @@ export default [ ...common }, { - name: "css modules & constants", + name: "css modules & constants & redux store slices", togglePointModule: "/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint", variantGlobs: [ "./src/routes/parallel-folder-convention/__variants__/*/components/**/*.css", - "./src/routes/parallel-folder-convention/__variants__/*/constants/**/*.ts" - ], - ...common - }, - { - name: "redux slices", - togglePointModule: - "/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint", - variantGlobs: [ + "./src/routes/parallel-folder-convention/__variants__/*/constants/**/*.ts", "./src/routes/parallel-folder-convention/__variants__/*/state/modules/*/slice.ts" ], ...common diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts index 256960a..8f43928 100644 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts @@ -3,14 +3,9 @@ import { FEATURE_KEY } from "../constants"; const getRelevantModule = (joinPoint, featuresMap) => { const activeFeatures = featuresMap.get(FEATURE_KEY); - try { - const { selection } = getFeatures(); - - const variant = activeFeatures.get(selection); - return variant ?? joinPoint; - } catch { - return joinPoint; - } + const { selection } = getFeatures(); + const variant = activeFeatures.get(selection); + return variant ?? joinPoint; }; const togglePoint = (joinPoint, featuresMap) => { diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts deleted file mode 100644 index 2bfd06f..0000000 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxSliceTogglePoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -import objectProxyTogglePoint from "./objectProxyTogglePoint"; -import { combineReducers } from "@reduxjs/toolkit"; -import { getFeatures } from "../featuresStore"; -import { subscribe } from "valtio"; - -if (CLIENT) { - subscribe(getFeatures(), () => { - delete require.cache[require.resolve("@/state/modules")]; - const { default: newReducerMap } = require("@/state/modules"); - const { default: store } = require("@/state/store"); - store.replaceReducer(combineReducers(newReducerMap)); - }); -} - -export default objectProxyTogglePoint; From bd432ac886d7333ba7da1b8e7a4a39ef144f7f79 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:30:27 +0100 Subject: [PATCH 22/44] update README --- .../express/src/routes/parallel-folder-convention/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md index 88bd08a..8ffdd17 100644 --- a/examples/express/src/routes/parallel-folder-convention/README.md +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -12,7 +12,7 @@ An inbound header named "feature" can take the following values: ...which is used server side (via a ["node request scoped features store"](../../../../../packages/features/docs/README.md#noderequestscopedfeaturesstorefactory)) to generate appropriate server-rendered content. -The chosen feature state is serialized to the browser using the `ssr` package, and loaded into a ["global features store"](../../../../../packages/features/docs/README.md#globalfeaturesstorefactory), using [`valtio`](https://github.com/pmndrs/valtio), for reactivity. +The chosen feature state is serialized to the browser using the `ssr` package, and loaded into a ["global features store"](../../../../../packages/features/docs/README.md#globalfeaturesstorefactory), using [`valtio`](https://github.com/pmndrs/valtio) browser-side, for reactivity. To demonstrate the reactivity, a drop-down allows changing of the selected feature state. @@ -120,3 +120,4 @@ To vary CSS files, constants, and [`redux` "slices"](https://redux.js.org/tutori To ensure that the redux store is reactive to feature state, a `StateProvider` react component is used to host the [react-redux provider](https://react-redux.js.org/api/provider), that subscribes to the valtio state, and calls [replaceReducer](https://redux.js.org/usage/code-splitting#using-replacereducer) from the [`@reduxjs/toolkit`](https://github.com/reduxjs/redux-toolkit) whenever the feature state changes. This ensures a new redux store (but retaining current state) is made available to the react components. Care should be taken to ensure the existing state is compatible with any updated selectors etc. +N.B. It is assumed that feature state will not change during a request cycle on the server, so `valtio` is only plumbed in to the client-side feature store, via conditional compilation using [Webpack's `DefinePlugin`](https://webpack.js.org/plugins/define-plugin/). \ No newline at end of file From 503a06e1a7296e7f2a995b8d53525a629e7335d2 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 22 Aug 2025 19:43:41 +0100 Subject: [PATCH 23/44] add "feature 5" to add a new redux slice entirely --- .../routes/parallel-folder-convention/App.tsx | 1 + .../parallel-folder-convention/README.md | 19 +++ .../feature5/components/TopBox/index.tsx | 19 +++ .../components/TopBox/styles.module.css | 122 ++++++++++++++++++ .../components/TopBox/useSpaceStuff.ts | 7 + .../feature5/state/modules/index.ts | 9 ++ .../feature5/state/modules/space/slice.ts | 16 +++ .../toggle-plumbing/pointCutConfig.js | 11 +- .../toggle-points/getRelevantModule.ts | 11 ++ .../toggle-points/objectProxyTogglePoint.ts | 10 +- .../reduxReducerMapTogglePoint.ts | 8 ++ 11 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/index.tsx create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/useSpaceStuff.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/index.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/space/slice.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/getRelevantModule.ts create mode 100644 examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxReducerMapTogglePoint.ts diff --git a/examples/express/src/routes/parallel-folder-convention/App.tsx b/examples/express/src/routes/parallel-folder-convention/App.tsx index 339d69b..ce1b235 100644 --- a/examples/express/src/routes/parallel-folder-convention/App.tsx +++ b/examples/express/src/routes/parallel-folder-convention/App.tsx @@ -16,6 +16,7 @@ export default function App() { + diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md index 8ffdd17..d344d5f 100644 --- a/examples/express/src/routes/parallel-folder-convention/README.md +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -110,6 +110,23 @@ Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with replaced └── slice.ts ``` +### _feature5_ + +Varied component (`TopBox`) & redux store (`modules/index.ts`) that introduces a new "space" slice, with it's own state containing spacey stuff. + +```bash +├── components +│ └── TopBox +│ ├── index.tsx +│ ├── styles.module.css +│ └── useSpaceStuff.ts +└── state + └── modules + ├── space + │ └── slice.ts + └── index.ts +``` + ## Explanation The webpack plugin is configured with a toggle handler that maps variants to controls based on a parallel root folder. This allows for any file to be replaced at any depth. The example shows both complete replacements, and augmentations (importing of the base, then modifying). @@ -118,6 +135,8 @@ To vary react components, the toggle point from the `react-pointcuts` package is To vary CSS files, constants, and [`redux` "slices"](https://redux.js.org/tutorials/essentials/part-2-app-structure#redux-slices), a toggle point is used that utilises an [object proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), to intercept property access. Despite objects not being innately reactive, as long as they are accessed from something that does update with change of state (i.e. react components), their properties will also update based on the new feature. +To vary `redux` reducer map, a toggle point that wraps the factory method (`getReducerMap`) is used. + To ensure that the redux store is reactive to feature state, a `StateProvider` react component is used to host the [react-redux provider](https://react-redux.js.org/api/provider), that subscribes to the valtio state, and calls [replaceReducer](https://redux.js.org/usage/code-splitting#using-replacereducer) from the [`@reduxjs/toolkit`](https://github.com/reduxjs/redux-toolkit) whenever the feature state changes. This ensures a new redux store (but retaining current state) is made available to the react components. Care should be taken to ensure the existing state is compatible with any updated selectors etc. N.B. It is assumed that feature state will not change during a request cycle on the server, so `valtio` is only plumbed in to the client-side feature store, via conditional compilation using [Webpack's `DefinePlugin`](https://webpack.js.org/plugins/define-plugin/). \ No newline at end of file diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/index.tsx b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/index.tsx new file mode 100644 index 0000000..ffc9777 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/index.tsx @@ -0,0 +1,19 @@ +import type { JSX } from "react"; +import Variant2 from "@/component-library/variant2"; +import useSpaceStuff from "./useSpaceStuff"; +import styles from "./styles.module.css"; + +const BottomBox = (): JSX.Element => { + const planets = useSpaceStuff() as string[]; + return ( + +
+ {planets.map((planet) => ( + {planet} + ))} +
+
+ ); +}; + +export default BottomBox; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css new file mode 100644 index 0000000..0b51709 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css @@ -0,0 +1,122 @@ +.space { + padding: 1em; + margin-top: 0.3em; + font-size: xxx-large; + position: relative; + display: inline-flex; + gap: 0.25em; + width: 180px; + height: 200px; + justify-content: center; + align-items: center; + border-radius: 1rem; + overflow: hidden; + + background: radial-gradient( + ellipse at center, + rgba(0, 0, 0, 1) 0%, + rgba(0, 0, 0, 0.85) 45%, + rgba(0, 0, 0, 0.5) 70%, + rgba(0, 0, 0, 0) 100% + ); +} + +/* Sparse starfield */ +.space::after { + content: ""; + position: absolute; + inset: -25%; + pointer-events: none; + + /* Few layers, spread wider = sparse stars */ + background-image: radial-gradient( + rgba(255, 255, 255, 0.9) 1px, + transparent 1.2px + ), + radial-gradient(rgba(255, 255, 255, 0.6) 1px, transparent 1.4px); + background-size: + 120px 120px, + 180px 180px; + background-position: + 0 0, + 60px 90px; + + animation: + star-drift 90s linear infinite, + twinkle 4s ease-in-out infinite alternate; + opacity: 0.8; +} + +/* Emojis float above the background */ +.space > * { + position: relative; + display: inline-block; + will-change: transform; + animation: float-ellipse 5s linear infinite; +} + +/* Phase offsets */ +.space > *:nth-child(3n + 1) { + animation-delay: 0s; +} +.space > *:nth-child(3n + 2) { + animation-delay: -1.67s; +} +.space > *:nth-child(3n + 3) { + animation-delay: -3.33s; +} + +@media (prefers-reduced-motion: reduce) { + .space > * { + animation: none; + } + .space::after { + animation: none; + } +} + +/* Elliptical drift (very subtle) */ +@keyframes float-ellipse { + 0% { + transform: translate(0.48em, 0em); + } + 25% { + transform: translate(0em, 0.32em); + } + 50% { + transform: translate(-0.48em, 0em); + } + 75% { + transform: translate(0em, -0.32em); + } + 100% { + transform: translate(0.48em, 0em); + } +} + +/* Gentle drift of the whole star layer */ +@keyframes star-drift { + 0% { + background-position: + 0px 0px, + 60px 90px; + } + 100% { + background-position: + -40px -30px, + 20px 50px; + } +} + +/* Soft twinkle effect */ +@keyframes twinkle { + 0% { + opacity: 0.6; + } + 50% { + opacity: 0.9; + } + 100% { + opacity: 0.5; + } +} diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/useSpaceStuff.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/useSpaceStuff.ts new file mode 100644 index 0000000..c1ac5fa --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/useSpaceStuff.ts @@ -0,0 +1,7 @@ +import useAppSelector from "@/state/useAppSelector"; +import slice from "../../state/modules/space/slice"; + +const useSpaceStuff = () => + useAppSelector(slice.selectors.getSpaceStuff as any); + +export default useSpaceStuff; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/index.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/index.ts new file mode 100644 index 0000000..95e84db --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/index.ts @@ -0,0 +1,9 @@ +import animalsSlice from "@/state/modules/animals/slice"; +import spaceSlice from "./space/slice"; + +const getReducerMap = () => ({ + animals: animalsSlice.reducer, + space: spaceSlice.reducer +}); + +export default getReducerMap; diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/space/slice.ts b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/space/slice.ts new file mode 100644 index 0000000..f98dc5d --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/state/modules/space/slice.ts @@ -0,0 +1,16 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const spaceSlice = createSlice({ + name: "space", + initialState: ["🌎", "🪐", "☄️", "🛸"], + reducers: { + add(state, action) { + state.push(action.payload); + } + }, + selectors: { + getSpaceStuff: (state) => state + } +}); + +export default spaceSlice; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js index eb05638..5c6b44c 100644 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/pointCutConfig.js @@ -21,7 +21,7 @@ export default [ ...common }, { - name: "css modules & constants & redux store slices", + name: "css modules & constants & redux slices", togglePointModule: "/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint", variantGlobs: [ @@ -30,5 +30,14 @@ export default [ "./src/routes/parallel-folder-convention/__variants__/*/state/modules/*/slice.ts" ], ...common + }, + { + name: "redux reducer maps", + togglePointModule: + "/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxReducerMapTogglePoint", + variantGlobs: [ + "./src/routes/parallel-folder-convention/__variants__/*/state/modules/index.ts" + ], + ...common } ]; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/getRelevantModule.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/getRelevantModule.ts new file mode 100644 index 0000000..ab64ae6 --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/getRelevantModule.ts @@ -0,0 +1,11 @@ +import { getFeatures } from "../featuresStore"; +import { FEATURE_KEY } from "../constants"; + +const getRelevantModule = (joinPoint, featuresMap) => { + const activeFeatures = featuresMap.get(FEATURE_KEY); + const { selection } = getFeatures(); + const variant = activeFeatures.get(selection); + return variant ?? joinPoint; +}; + +export default getRelevantModule; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts index 8f43928..a6a50b3 100644 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/objectProxyTogglePoint.ts @@ -1,12 +1,4 @@ -import { getFeatures } from "../featuresStore"; -import { FEATURE_KEY } from "../constants"; - -const getRelevantModule = (joinPoint, featuresMap) => { - const activeFeatures = featuresMap.get(FEATURE_KEY); - const { selection } = getFeatures(); - const variant = activeFeatures.get(selection); - return variant ?? joinPoint; -}; +import getRelevantModule from "./getRelevantModule"; const togglePoint = (joinPoint, featuresMap) => { return new Proxy(joinPoint.default, { diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxReducerMapTogglePoint.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxReducerMapTogglePoint.ts new file mode 100644 index 0000000..2fc0f7d --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/toggle-points/reduxReducerMapTogglePoint.ts @@ -0,0 +1,8 @@ +import getRelevantModule from "./getRelevantModule"; + +const togglePoint = + (joinPoint, featuresMap) => + (...args) => + getRelevantModule(joinPoint, featuresMap).default(...args); + +export default togglePoint; From 721b71196cbcbaaccbbf90123f94992684d94b77 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Sat, 23 Aug 2025 09:22:19 +0100 Subject: [PATCH 24/44] move getFeatures into serialization plumbing --- .../src/routes/parallel-folder-convention/router.tsx | 8 ++++---- .../toggle-plumbing/serialization.ts | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/express/src/routes/parallel-folder-convention/router.tsx b/examples/express/src/routes/parallel-folder-convention/router.tsx index 9e9c80e..dde0de5 100644 --- a/examples/express/src/routes/parallel-folder-convention/router.tsx +++ b/examples/express/src/routes/parallel-folder-convention/router.tsx @@ -1,14 +1,14 @@ import { Router, static as assetsFolder } from "express"; import { renderToPipeableStream } from "react-dom/server"; -import { getScriptReactElement } from "./toggle-plumbing/serialization"; -import { setValue, getFeatures } from "./toggle-plumbing/featuresStore"; +import { getFeaturesScript } from "./toggle-plumbing/serialization"; +import { setValue } from "./toggle-plumbing/featuresStore"; import App from "./App"; const router = new Router(); router.use(assetsFolder("public")); router.use("/*", (request, _, scopeCallBack) => { setValue({ - value: { selection: request.headers.feature || "baseline" }, + value: { selection: request.headers.feature ?? "baseline" }, scopeCallBack }); }); @@ -28,7 +28,7 @@ router.get("/*", (_, res) => { rel="stylesheet" /> - {getScriptReactElement({ content: getFeatures() })} + {getFeaturesScript()}
diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts index 5f754a1..9beb93c 100644 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/serialization.ts @@ -1,8 +1,12 @@ import { serializationFactory } from "@asos/web-toggle-point-ssr"; +import { getFeatures } from "./featuresStore"; const { getScriptReactElement, getJSONFromScript } = serializationFactory({ id: "features", logWarning: console.warn }); -export { getScriptReactElement, getJSONFromScript }; +const getFeaturesScript = () => + getScriptReactElement({ content: getFeatures() }); + +export { getFeaturesScript, getJSONFromScript }; From e371203b331303de38aa9e1ebfbef0e33574c8e6 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Sat, 23 Aug 2025 09:24:52 +0100 Subject: [PATCH 25/44] remove chatGPTs helpful comments --- .../feature5/components/TopBox/styles.module.css | 8 -------- 1 file changed, 8 deletions(-) diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css index 0b51709..e4e062e 100644 --- a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css @@ -21,14 +21,12 @@ ); } -/* Sparse starfield */ .space::after { content: ""; position: absolute; inset: -25%; pointer-events: none; - /* Few layers, spread wider = sparse stars */ background-image: radial-gradient( rgba(255, 255, 255, 0.9) 1px, transparent 1.2px @@ -47,7 +45,6 @@ opacity: 0.8; } -/* Emojis float above the background */ .space > * { position: relative; display: inline-block; @@ -55,7 +52,6 @@ animation: float-ellipse 5s linear infinite; } -/* Phase offsets */ .space > *:nth-child(3n + 1) { animation-delay: 0s; } @@ -74,8 +70,6 @@ animation: none; } } - -/* Elliptical drift (very subtle) */ @keyframes float-ellipse { 0% { transform: translate(0.48em, 0em); @@ -94,7 +88,6 @@ } } -/* Gentle drift of the whole star layer */ @keyframes star-drift { 0% { background-position: @@ -108,7 +101,6 @@ } } -/* Soft twinkle effect */ @keyframes twinkle { 0% { opacity: 0.6; From c4d64bceff720a68af31dd406c4f42c4b1b6fa45 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:56:40 +0100 Subject: [PATCH 26/44] match sizes of the TopBox --- .../__variants__/feature5/components/TopBox/styles.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css index e4e062e..4e17879 100644 --- a/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css +++ b/examples/express/src/routes/parallel-folder-convention/__variants__/feature5/components/TopBox/styles.module.css @@ -5,8 +5,8 @@ position: relative; display: inline-flex; gap: 0.25em; - width: 180px; - height: 200px; + width: 185px; + height: 170px; justify-content: center; align-items: center; border-radius: 1rem; From 63f33b7f332e4d1f72724f15858cd284808e448c Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:25:07 +0100 Subject: [PATCH 27/44] merge main --- .npmrc | 1 + docs/CHANGELOG.md | 36 +- docs/README.md | 2 +- examples/express/docs/CHANGELOG.md | 31 +- examples/express/package.json | 8 +- .../src/routes/animals/featuresStore.js | 3 +- .../src/routes/animals/playwright.spec.js | 2 +- .../express/src/routes/config/Component.jsx | 2 +- .../div-style/Large/Component.jsx | 2 +- .../div-style/Small/Component.jsx | 2 +- .../src/routes/config/featuresStore.js | 2 +- .../src/routes/config/playwright.spec.js | 4 +- .../express/src/routes/config/togglePoint.js | 6 +- .../toggle-plumbing/featuresStore/browser.ts | 4 +- .../toggle-plumbing/featuresStore/server.ts | 5 +- examples/next/README.md | 46 +- examples/next/docs/CHANGELOG.md | 43 +- examples/next/next-env.d.ts | 1 + examples/next/next.config.mjs | 14 +- examples/next/package.json | 17 +- .../fixtures/content-management/README.mdx | 18 + .../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 | 14 + .../experiments/1-varied-component/README.mdx | 4 + .../1-varied-component/playwright.spec.ts | 6 +- .../README.mdx | 4 + .../playwright.spec.ts | 2 +- .../README.mdx | 4 + .../playwright.spec.ts | 6 +- .../experiments/4-varied-variant/README.mdx | 4 + .../4-varied-variant/playwright.spec.ts | 8 +- .../5-toggle-config-opt-out/README.mdx | 4 + .../playwright.spec.ts | 6 +- .../README.mdx | 4 + .../playwright.spec.ts | 10 +- .../README.mdx | 4 + .../playwright.spec.ts | 2 +- .../experiments/8-toggled-twice/README.mdx | 32 + .../test-variant/__pointCutConfig.js | 7 + .../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 | 33 + .../test-variant/withTogglePoint.tsx | 41 + .../experiments/8-toggled-twice/component.tsx | 1 + .../experiments/8-toggled-twice/page.tsx | 16 + .../8-toggled-twice/playwright.spec.ts | 51 + .../src/app/fixtures/experiments/README.md | 35 + .../fixtures/experiments/__pointCutConfig.js | 12 + .../src/app/fixtures/experiments/example.tsx | 2 +- .../app/fixtures/experiments/featuresStore.ts | 4 +- .../fixtures/experiments/getExperiments.ts | 4 +- .../src/app/fixtures/experiments/layout.tsx | 2 +- .../fixtures/experiments/withTogglePoint.ts | 2 +- .../playwright.getFixtureUrl.ts | 0 examples/next/src/app/layout.tsx | 2 +- examples/next/src/components/control1.tsx | 2 +- examples/next/src/components/control2.tsx | 2 +- examples/next/src/components/variant1.tsx | 2 +- examples/next/src/components/variant2.tsx | 2 +- examples/next/tsconfig.json | 18 +- examples/serve/docs/CHANGELOG.md | 15 +- examples/serve/package.json | 8 +- examples/serve/playwright.config.ts | 2 +- .../src/fixtures/audience/__featuresStore.js | 7 +- .../src/fixtures/config/__featuresStore.js | 7 +- .../src/fixtures/event/__featuresStore.js | 7 +- .../-screenshots-pride-chromium-win32.png | Bin 5015 -> 4426 bytes .../fixtures/translation/__featuresStore.js | 7 +- package-lock.json | 8055 +++++++++++++---- package.json | 10 +- packages/eslint.config.mjs | 6 + packages/features/.npmignore | 3 - .../build/eslint-import-backfill.rollup.mjs | 9 + packages/features/build/rollup.mjs | 38 +- packages/features/docs/CHANGELOG.md | 51 +- packages/features/docs/README.md | 60 +- packages/features/package.json | 45 +- packages/features/src/browser.js | 4 - packages/features/src/global.js | 12 +- packages/features/src/node.js | 5 - .../features/src/nodeRequestScoped/store.js | 33 - .../src/nodeRequestScoped/store.test.js | 57 - .../globalFeaturesStoreFactory.js} | 9 +- .../globalFeaturesStoreFactory.test.js} | 15 +- .../nodeRequestScopedFeaturesStoreFactory.js | 44 + ...eRequestScopedFeaturesStoreFactory.test.js | 85 + .../reactContextFeaturesStoreFactory.js} | 7 +- .../reactContextFeaturesStoreFactory.test.js} | 12 +- ...BackedReactContextFeaturesStoreFactory.js} | 19 +- ...dReactContextFeaturesStoreFactory.test.js} | 30 +- packages/prepare-publish.mjs | 23 + packages/react-pointcuts/.npmignore | 3 - packages/react-pointcuts/build/rollup.mjs | 4 +- packages/react-pointcuts/docs/CHANGELOG.md | 34 + packages/react-pointcuts/package.json | 27 +- packages/react-pointcuts/src/external.js | 11 - .../src/getCodeSelectionPlugins.test.js | 18 - ...ctionPlugins.js => getHooksFromPlugins.js} | 8 +- .../src/getHooksFromPlugins.test.js | 31 + .../getComponent/index.js | 26 +- .../getComponent/index.test.js | 43 +- ...Plugins.js => withCodeSelectionPlugins.js} | 10 +- ...st.js => withCodeSelectionPlugins.test.js} | 18 +- .../getComponent/withErrorBoundary.js | 4 +- .../getComponent/withErrorBoundary.test.js | 14 +- .../src/withTogglePointFactory/index.js | 35 +- .../src/withTogglePointFactory/index.test.js | 33 +- .../src/withToggledHookFactory/index.js | 19 +- .../src/withToggledHookFactory/index.test.js | 167 +- .../pluginsHookFactory.js | 18 +- .../pluginsHookFactory.test.js | 71 +- packages/ssr/.npmignore | 3 - packages/ssr/build/rollup.mjs | 4 +- packages/ssr/docs/CHANGELOG.md | 25 + packages/ssr/package.json | 29 +- .../ssr/src/serializationFactory/index.js | 21 +- .../src/serializationFactory/index.test.js | 14 +- packages/webpack/.npmignore | 3 - packages/webpack/docs/CHANGELOG.md | 28 +- packages/webpack/docs/README.md | 14 +- packages/webpack/package.json | 16 +- .../src/plugins/togglePointInjection/index.js | 2 +- .../resourceProxyExistsInRequestChain.js | 2 +- .../resolveJoinPoints/index.js | 24 +- .../resolveJoinPoints/index.test.js | 59 +- peripheral/babel-preset-asos/package.json | 4 - .../browserslist-config-asos/package.json | 4 - test/automation/docs/CHANGELOG.md | 10 +- test/automation/package.json | 7 +- 140 files changed, 7880 insertions(+), 2460 deletions(-) create mode 100644 .npmrc 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 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 create mode 100644 examples/next/src/app/fixtures/experiments/__pointCutConfig.js rename examples/next/src/app/fixtures/{experiments => }/playwright.getFixtureUrl.ts (100%) create mode 100644 packages/features/build/eslint-import-backfill.rollup.mjs delete mode 100644 packages/features/src/browser.js delete mode 100644 packages/features/src/node.js delete mode 100644 packages/features/src/nodeRequestScoped/store.js delete mode 100644 packages/features/src/nodeRequestScoped/store.test.js rename packages/features/src/{global/store.js => storeFactories/globalFeaturesStoreFactory.js} (80%) rename packages/features/src/{global/store.test.js => storeFactories/globalFeaturesStoreFactory.test.js} (68%) create mode 100644 packages/features/src/storeFactories/nodeRequestScopedFeaturesStoreFactory.js create mode 100644 packages/features/src/storeFactories/nodeRequestScopedFeaturesStoreFactory.test.js rename packages/features/src/{reactContext/store.js => storeFactories/reactContextFeaturesStoreFactory.js} (74%) rename packages/features/src/{reactContext/store.test.js => storeFactories/reactContextFeaturesStoreFactory.test.js} (69%) rename packages/features/src/{ssrBackedReactContext/store.js => storeFactories/ssrBackedReactContextFeaturesStoreFactory.js} (62%) rename packages/features/src/{ssrBackedReactContext/store.test.js => storeFactories/ssrBackedReactContextFeaturesStoreFactory.test.js} (80%) create mode 100644 packages/prepare-publish.mjs delete mode 100644 packages/react-pointcuts/src/getCodeSelectionPlugins.test.js rename packages/react-pointcuts/src/{getCodeSelectionPlugins.js => getHooksFromPlugins.js} (62%) create mode 100644 packages/react-pointcuts/src/getHooksFromPlugins.test.js rename packages/react-pointcuts/src/withTogglePointFactory/getComponent/{withPlugins.js => withCodeSelectionPlugins.js} (71%) rename packages/react-pointcuts/src/withTogglePointFactory/getComponent/{withPlugins.test.js => withCodeSelectionPlugins.test.js} (80%) diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..38f11c6 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9ce5f67..3b8b933 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,13 +14,47 @@ 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.7] - 2025-07-29 +## [0.13.0] - 2025-10-21 ### Changed - updated to `0.11.0` of [`eslint-plugin-workspaces`](https://github.com/joshuajaco/eslint-plugin-workspaces) after [addition of ESLint9 support](https://github.com/joshuajaco/eslint-plugin-workspaces/commit/af855c3a3d8069366d4446747e91828ddf7560c6) - update `eslint.config.mjs` to utilise flat config +## [0.12.0] - 2025-09-30 + +### Added + +- a `build:examples` script, for easy re-building of all examples + +### Fixed + +- added a `prepare-publish.mjs` script to prepare packages for npm publishing (moving `README.md` and `LICENSE` files into package roots), replacing the prior `shx` based solution, and adding [`transform-markdown-links`](https://github.com/gakimball/transform-markdown-links) to fix relative paths that otherwise incorrectly link on [npmjs.com](https://www.npmjs.com/) + +### Changed + +- updated [`danger-js`](https://github.com/danger/danger-js) to [version 13.0.3](https://github.com/danger/danger-js/blob/main/CHANGELOG.md#1303), to remove high vulnerabilities + +## [0.11.0] - 2025-09-29 + +### Changed + +- removed the `tags` badge from main `README.md`, this monorepo does not use release tags + +### Added + +- a `discussion` badge + +## [0.10.7] - 2025-07-14 + +### Fixed + +- removed `keywords` from `package.json` (in package root and peripherals), since serving no purpose in private packages + +### Added + +- `.npmrc` for ensuring correct registry + ## [0.10.6] - 2025-07-14 ### Fixed diff --git a/docs/README.md b/docs/README.md index a59e5f2..f8be1d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,10 +5,10 @@ A library providing a means to toggle or branch web application code.

- Current version Current test status PRs are welcome toggle point issues + toggle point discussions toggle point stars toggle point forks toggle point license diff --git a/examples/express/docs/CHANGELOG.md b/examples/express/docs/CHANGELOG.md index e48b005..1937dc2 100644 --- a/examples/express/docs/CHANGELOG.md +++ b/examples/express/docs/CHANGELOG.md @@ -5,24 +5,49 @@ 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] - 2025-08-21 +## [0.4.0] - 2025-10-21 ### Changed - added "parallel folder convention" example -## [0.2.6] - 2025-07-29 +### Fixed + +- went ltd cmdr data on the contraction "I'm", converting to "I am" since the "large" example inexplicably no longer text-matched (space before apostrophe) + +## [0.3.1] - 2025-10-21 ### Fixed - removed "Vary" header from "animals" example, the page is meant to be un-cacheable, and the value was wrong in any case + +## [0.3.0] - 2025-10-20 + +### Changed + +- updated to features [version 0.5.0](../../../packages/features/docs/CHANGELOG.md#050---2025-10-20) + +## [0.2.7] - 2025-09-30 + +### Changed + +- move to use new `onVariantError` plugin hook over `logError` to consume [0.5.0](../../../packages/react-pointcuts/docs/CHANGELOG.md#040---2025-07-06) of `react-pointcuts` package. + +## [0.2.6] - 2025-07-15 + +### Changed + +- Updated to named exports version of "features" package ([0.4.0](../../../packages/features/docs/CHANGELOG.md#040---2025-07-15)) + +### Fixed + - import types explicitly from `@playwright/test` after unexpected pipeline failure ## [0.2.5] - 2025-05-27 ### Changed -- updated to use `variantGlobs` array, with updated webpack plugin [0.8.0][version 0.8.0](../../../packages/webpack/docs/CHANGELOG.md#080---2025-05-27) +- updated to use `variantGlobs` array, with updated webpack plugin [version 0.8.0](../../../packages/webpack/docs/CHANGELOG.md#080---2025-05-27) - used some differing syntax from [`micromatch`](https://github.com/micromatch/micromatch) to define `variantGlobs`, for coverage and where may be preferred ## [0.2.4] - 2024-02-07 diff --git a/examples/express/package.json b/examples/express/package.json index e2271c3..0bf68b1 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-express-example", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "engines": { "node": ">=20.6.0" @@ -16,8 +16,8 @@ "prelint": "npm run build-dependencies", "lint": "npm run lint:code && npm run lint:docs", "lint:fix": "npm run lint:code -- --fix && npm run lint:docs -- --fix", - "lint:code": "eslint src --flag unstable_config_lookup_from_file", - "lint:docs": "eslint *.md --flag unstable_config_lookup_from_file" + "lint:code": "eslint src --flag v10_config_lookup_from_file", + "lint:docs": "eslint *.md --flag v10_config_lookup_from_file" }, "dependencies": { "@asos/web-toggle-point-features": "file:../../packages/features", @@ -48,6 +48,6 @@ "webpack-node-externals": "^3.0.0" }, "peerDependencies": { - "@playwright/test": "^1.49.0" + "@playwright/test": "^1.56.0" } } diff --git a/examples/express/src/routes/animals/featuresStore.js b/examples/express/src/routes/animals/featuresStore.js index dc28cf0..c6003a8 100644 --- a/examples/express/src/routes/animals/featuresStore.js +++ b/examples/express/src/routes/animals/featuresStore.js @@ -1,5 +1,6 @@ +// eslint-disable-next-line import/no-unresolved -- https://github.com/import-js/eslint-plugin-import/issues/1810 import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/nodeRequestScopedFeaturesStoreFactory"; -const featuresStore = featuresStoreFactory(); +const featuresStore = featuresStoreFactory({ toggleType: "api version" }); export default featuresStore; diff --git a/examples/express/src/routes/animals/playwright.spec.js b/examples/express/src/routes/animals/playwright.spec.js index 7cadd10..8a8b0b6 100644 --- a/examples/express/src/routes/animals/playwright.spec.js +++ b/examples/express/src/routes/animals/playwright.spec.js @@ -38,7 +38,7 @@ test.describe("versioned image endpoint", () => { await page.goto("http://localhost:3002/animals"); await expect( page.locator(`img[data-version="${version}"]`) - ).toBeInViewport(); + ).toBeVisible(); }); }); }); diff --git a/examples/express/src/routes/config/Component.jsx b/examples/express/src/routes/config/Component.jsx index a9c318b..4daf9e2 100644 --- a/examples/express/src/routes/config/Component.jsx +++ b/examples/express/src/routes/config/Component.jsx @@ -2,7 +2,7 @@ const Component = () => (

- I'm Medium + I am Medium
); diff --git a/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx b/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx index 04a41e9..dcb4ebb 100644 --- a/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx +++ b/examples/express/src/routes/config/__variants__/div-style/Large/Component.jsx @@ -8,7 +8,7 @@ const Component = ({ backgroundColor }) => ( backgroundColor }} > - I 'm Large + I am Large
); diff --git a/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx b/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx index ecc46e7..c6cf9c0 100644 --- a/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx +++ b/examples/express/src/routes/config/__variants__/div-style/Small/Component.jsx @@ -8,7 +8,7 @@ const Component = ({ backgroundColor }) => ( backgroundColor }} > - I'm Small + I am Small ); diff --git a/examples/express/src/routes/config/featuresStore.js b/examples/express/src/routes/config/featuresStore.js index cd44351..52c205c 100644 --- a/examples/express/src/routes/config/featuresStore.js +++ b/examples/express/src/routes/config/featuresStore.js @@ -1,7 +1,7 @@ import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/ssrBackedReactContextFeaturesStoreFactory"; const featuresStore = featuresStoreFactory({ - name: "config", + toggleType: "config", logWarning: console.log }); diff --git a/examples/express/src/routes/config/playwright.spec.js b/examples/express/src/routes/config/playwright.spec.js index 85f4483..edb4a71 100644 --- a/examples/express/src/routes/config/playwright.spec.js +++ b/examples/express/src/routes/config/playwright.spec.js @@ -14,7 +14,7 @@ test.describe("config endpoint", () => { }); test("it shows a varied experience", async ({ page }) => { - await expect(page.getByText(`I'm ${size}`)).toBeInViewport(); + await expect(page.getByText(`I am ${size}`)).toBeVisible(); }); scenarios @@ -25,7 +25,7 @@ test.describe("config endpoint", () => { page }) => { await page.getByRole("button", { name: otherSize }).click(); - await expect(page.getByText(`I'm ${otherSize}`)).toBeInViewport(); + await expect(page.getByText(`I am ${otherSize}`)).toBeVisible(); }); }); }); diff --git a/examples/express/src/routes/config/togglePoint.js b/examples/express/src/routes/config/togglePoint.js index abdcd3b..1ae5186 100644 --- a/examples/express/src/routes/config/togglePoint.js +++ b/examples/express/src/routes/config/togglePoint.js @@ -6,7 +6,11 @@ const { getFeatures: getActiveFeatures } = featuresStore; const withTogglePoint = withTogglePointFactory({ getActiveFeatures, variantKey: "size", - logError: console.log + plugins: [ + { + onVariantError: console.error + } + ] }); export default withTogglePoint; diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts index 5feee19..25dba54 100644 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/browser.ts @@ -2,7 +2,9 @@ import { proxy, useSnapshot } from "valtio"; import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/globalFeaturesStoreFactory"; import { getJSONFromScript } from "../serialization"; -const { setValue: storeSetValue, getFeatures } = featuresStoreFactory(); +const { setValue: storeSetValue, getFeatures } = featuresStoreFactory({ + toggleType: "version" +}); storeSetValue({ value: proxy({}) }); export const setValue = (input) => { diff --git a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts index 3f15b50..8fbc3e2 100644 --- a/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts +++ b/examples/express/src/routes/parallel-folder-convention/toggle-plumbing/featuresStore/server.ts @@ -1,5 +1,8 @@ +// eslint-disable-next-line import/no-unresolved -- https://github.com/import-js/eslint-plugin-import/issues/1810 import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/nodeRequestScopedFeaturesStoreFactory"; -const { setValue, getFeatures } = featuresStoreFactory(); +const { setValue, getFeatures } = featuresStoreFactory({ + toggleType: "version" +}); export { getFeatures, setValue }; export const useFeatures = () => ({ feature: getFeatures() }); diff --git a/examples/next/README.md b/examples/next/README.md index 9ff4860..48a7229 100644 --- a/examples/next/README.md +++ b/examples/next/README.md @@ -1,48 +1,24 @@ # Next JS example -This example shows the use of the [`react-pointcuts`](../../packages/react-pointcuts/docs/README.md), [`features`](../../packages/features/docs/README.md) and [`webpack`](../../packages/webpack/docs/README.md) packages, as part of an ["app router"](https://nextjs.org/docs/app) [Next.js](https://nextjs.org/) application. +These examples show the use of the [`react-pointcuts`](../../packages/react-pointcuts/docs/README.md), [`features`](../../packages/features/docs/README.md) and [`webpack`](../../packages/webpack/docs/README.md) packages, as part of an ["app router"](https://nextjs.org/docs/app) [Next.js](https://nextjs.org/) application. N.B. NextJs support is currently experimental, see [caveats](#caveats). -## Setup +## Examples -It is using a contrived point cut plugin, replicating an Optimizely activation handler: +1. [content management](./src/app/fixtures/content-management/README.mdx) -```js -{ - onCodeSelected: ({ matchedFeatures }) => { - if (matchedFeatures?.length) { - const [[feature]] = matchedFeatures; - console.log( - `activated ${feature} with audience ${getFeatures().audience}` - ); - } - } -} -``` + This is a basic example demonstrating the ability to vary react hooks, using a contrived content management feature. -...which logs the activation event normally destined for the toggle router (Optimizely) to the console. +2. [experiments](./src/app/fixtures/experiments/README.mdx) -A contrived server function called `getExperiments` exists to parse inbound headers containing experiments, used to drive the toggling. - -## Usage - -(from the `examples/next` folder of the monorepo) - -1. install [mod header](https://modheader.com/), or some other tool for modifying request headers sent in a browser -2. `npm install` -4. `npm run dev` -5. open `localhost:3000/fixtures/experiments` in a browser - -See appropriate documentation within [the examples folder](./src/app/fixtures/experiments/README.md). - -N.B. To confirm the `experiments` header you've set with `mod header`, you can add `?showExperiments=true` to the URL to render the value to the top of the page. -If you're not seeing the experiments header show up, try refreshing the page. NextJs is perhaps pre-caching the pages. + These examples show various toggle setups with react components being varied, opting out of variation, etc. ## Caveats -- Only client components are supported, since request-bound context is not supported by server components. They are not meant to be stateful. - - API routes may be supportable, via use of [an async local storage wrapper](https://github.com/rexfordessilfie/nextwrappers/tree/main/packages/async-local-storage), once support for named exports is added ([Issue #4](https://github.com/ASOS/web-toggle-point/issues/4)) - since would need to match the HTTP verbs. -- The webpack package cannot currently vary NextJs managed files such as [pages](https://nextjs.org/docs/app/building-your-application/routing/pages) themselves, but can vary modules they import ([Issue #9](https://github.com/ASOS/web-toggle-point/issues/9)). +- Only client components can be toggled using a per-request features store, since request-bound context is not supported by server components. They are not meant to be stateful + - API routes may be supportable, via use of [an async local storage wrapper](https://github.com/rexfordessilfie/nextwrappers/tree/main/packages/async-local-storage), once support for named exports is added ([Issue #4](https://github.com/ASOS/web-toggle-point/issues/4)) - since would need to match the HTTP verbs +- The webpack package cannot currently vary some of NextJs' [filesystem convention files](https://nextjs.org/docs/pages/getting-started/project-structure#files-conventions) ([Issue #9](https://github.com/ASOS/web-toggle-point/issues/9)) - The `webpack` plugin uses webpack hooks, so is incompatible with the new TurboPack bundler -- The `webpack` plugin uses Node JS APIs to access the filesystem, so may be incompatible with [the edge runtime](https://nextjs.org/docs/app/api-reference/edge#unsupported-apis). +- The `webpack` plugin uses Node JS APIs to access the filesystem, so may be incompatible with [the edge runtime](https://nextjs.org/docs/app/api-reference/edge#unsupported-apis) +- The `nodeRequestScopedFeaturesStoreFactory` from the [`features`](../../packages/features/docs/README.md) package relies on singleton values held in top-level scope, which Next does not support. See [issue 50](https://github.com/ASOS/web-toggle-point/issues/50), which should remedy this. \ No newline at end of file diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index be1aa72..244ad2d 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -5,12 +5,48 @@ 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.5] - 2025-07-29 +## [0.4.1] - 2025-10-21 ### Changed - update to take supply static `webpackNormalModule` corresponding to webpack plugin [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) +## [0.4.0] - 2025-10-20 + +### Changed + +- updated to features [version 0.5.0](../../../packages/features/docs/CHANGELOG.md#050---2025-10-20) + +## [0.3.1] - 2025-09-30 + +### Changed + +- move to use new `onVariantError` plugin hook over `logError` to consume [0.5.0](../../../packages/react-pointcuts/docs/CHANGELOG.md#040---2025-07-06) of `react-pointcuts` package. + +## [0.3.0] - 2025-09-29 + +### Added + +- added a "content management" example, demonstrating use of `withToggledHookFactory` from the `react-pointcuts` package + +### Changed + +- colocate documentation for "experiments" example to sit with its own `README.mdx` +- update to static `webpackNormalModule` option of webpack package [version 0.8.1](../../../packages/webpack/docs/CHANGELOG.md#081---2025-07-27) +- Updated to named exports version of "features" package ([0.4.0](../../../packages/features/docs/CHANGELOG.md#040---2025-07-15)) + +### Fixed + +- consistent "Explanation" and "Activation" sections in example `README.mdx` files +- removed errant `toggle-point.d.ts` in `tsconfig.json` +- moved type packages to devDependencies + +## [0.2.5] - 2025-07-15 + +### Changed + +- Updated to named exports version of "features" package ([0.4.0](../../../packages/features/docs/CHANGELOG.md#040---2025-07-15)) + ### Fixed - import types explicitly from `@playwright/test` & internally, after unexpected pipeline failure @@ -19,19 +55,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- updated to use `variantGlobs` array, with updated webpack plugin [0.8.0][version 0.8.0](../../../packages/webpack/docs/CHANGELOG.md#080---2025-05-27) +- updated to use `variantGlobs` array, with updated webpack plugin [version 0.8.0](../../../packages/webpack/docs/CHANGELOG.md#080---2025-05-27) ## [0.2.3] - 2025-02-07 ### Fixed -- fixed a regression in the ability to use `?showExperiments` query in the experiments fixtures, regressed in version `0.2.0`. +- fixed a regression in the ability to use `?showExperiments` query in the experiments fixtures, regressed in [version 0.2.0](#020---2024-12-06). ## [0.2.2] - 2024-12-24 ### Fixed - links to folders, not `README.mdx`, in the experiments examples +- create `FeaturesProvider` via factory in outermost scope, rather than on each render of an example ## [0.2.1] - 2024-12-18 diff --git a/examples/next/next-env.d.ts b/examples/next/next-env.d.ts index 1b3be08..830fb59 100644 --- a/examples/next/next-env.d.ts +++ b/examples/next/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/next/next.config.mjs b/examples/next/next.config.mjs index 799b10b..afbacf1 100644 --- a/examples/next/next.config.mjs +++ b/examples/next/next.config.mjs @@ -1,5 +1,8 @@ import createMDX from "@next/mdx"; import remarkGfm from "remark-gfm"; +import { TogglePointInjection } from "@asos/web-toggle-point-webpack/plugins"; +import experimentPointCutConfig from "./src/app/fixtures/experiments/__pointCutConfig.js"; +import contentManagementPointCutConfig from "./src/app/fixtures/content-management/__pointCutConfig.js"; import webpackNormalModule from "next/dist/compiled/webpack/NormalModule.js"; /** @type {import('next').NextConfig} */ @@ -7,17 +10,8 @@ const nextConfig = { pageExtensions: ["js", "md", "mdx", "ts", "tsx"] }; -import { TogglePointInjection } from "@asos/web-toggle-point-webpack/plugins"; const togglePointInjection = new TogglePointInjection({ - pointCuts: [ - { - name: "experiments", - togglePointModule: "/src/app/fixtures/experiments/withTogglePoint", - variantGlobs: [ - "./src/app/fixtures/experiments/**/__variants__/*/*/!(*.spec).tsx" - ] - } - ], + pointCuts: [...experimentPointCutConfig, ...contentManagementPointCutConfig], webpackNormalModule }); diff --git a/examples/next/package.json b/examples/next/package.json index e1d85f0..5c9e4c2 100644 --- a/examples/next/package.json +++ b/examples/next/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-next-example", - "version": "0.2.4", + "version": "0.4.1", "private": true, "type": "module", "scripts": { @@ -17,26 +17,27 @@ "lint:docs": "eslint **/*.mdx" }, "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-webpack": "file:../../packages/webpack", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "^15.0.3", - "@types/mdx": "^2.0.13", - "next": "^15.0.3", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "remark-gfm": "^4.0.0" + "next": "^15.5.6", + "remark-gfm": "^4.0.0", + "turndown": "^7.2.0" }, "devDependencies": { "@next/eslint-plugin-next": "^15.0.3", + "@types/mdx": "^2.0.13", + "@types/turndown": "^5.0.5", + "@types/webpack-env": "^1.18.8", "eslint-config-next": "^15.0.3", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-mdx": "^3.1.5", "path-exists-cli": "^2.0.0" }, "peerDependencies": { - "@playwright/test": "^1.49.0" + "@playwright/test": "^1.56.0" } } 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..61d9b75 --- /dev/null +++ b/examples/next/src/app/fixtures/content-management/README.mdx @@ -0,0 +1,18 @@ +Content Management +================== + +Explanation +----------- + +This example demonstrates the `withToggledHookFactory` from the `react-pointcuts` package, and the `reactContextFeaturesStoreFactory` from the `features` package. + +A React hook is varied, from a ["no operation"](https://en.wikipedia.org/wiki/NOP_\(code\)) control version, to one that enables [design mode](https://developer.mozilla.org/en-US/docs/Web/API/Document/designMode) for the document. + +Activation +---------- + +If a cookie exists with the name `i-am-a-content-editor` and any value, on refreshing the page, the document becomes editable. Its background will be green to help indicate this state. + +After making changes, pressing [Command](https://en.wikipedia.org/wiki/Command_key) and S (⌘+S or ⊞+S, per O/S) will save changes back to disk, via a [server function](https://react.dev/reference/rsc/server-functions). The background will flash blue whilst save is in progress. + +N.B. The action uses [turndown](https://github.com/mixmark-io/turndown) to convert the HTML back to [Markdown](https://en.wikipedia.org/wiki/Markdown), so is limited by what that can interpret. \ No newline at end of file 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..bc97cb9 --- /dev/null +++ b/examples/next/src/app/fixtures/content-management/__pointCutConfig.js @@ -0,0 +1,9 @@ +export default [ + { + name: "content management", + togglePointModule: "/src/app/fixtures/content-management/withToggledHook", + variantGlobs: [ + "./src/app/fixtures/content-management/__variants__/devMode/active/useContentEditable.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..e61c907 --- /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(null); + + 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(/`; +}; + // eslint-disable-next-line prettier/prettier, no-empty -- https://github.com/babel/babel/issues/15156 {} /** @@ -53,14 +60,14 @@ const serializationFactory = ({ id, logWarning }) => * @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. */ - getScriptMarkup({ content }) { - return ``; - }, + getScriptMarkup: getScriptMarkup.bind(null, id), + /** + * @memberof module:web-toggle-point-ssr.serialization + * @param {object} content The JSON content to be serialized. + * @returns {external:React.Component} A react component for rendering a type="application/json" script element with the specified content. + */ getScriptReactElement(...args) { - return parse(this.getScriptMarkup(...args)); + return parse(getScriptMarkup.apply(null, [id, ...args])); }, getJSONFromScript() { const input = document.querySelector(`#${id}`)?.textContent; diff --git a/packages/ssr/src/serializationFactory/index.test.js b/packages/ssr/src/serializationFactory/index.test.js index 7bcd23b..ce72ef8 100644 --- a/packages/ssr/src/serializationFactory/index.test.js +++ b/packages/ssr/src/serializationFactory/index.test.js @@ -27,7 +27,8 @@ describe("serializationFactory", () => { let result; beforeEach(() => { - result = serialization.getScriptMarkup({ content }); + const { getScriptMarkup } = serialization; + result = getScriptMarkup({ content }); }); it("should return a string containing a JSON script tag, appropriately quoted by jsesc", () => { @@ -44,9 +45,8 @@ describe("serializationFactory", () => { describe("when rendering a scriptReactElement with React", () => { let result; beforeEach(() => { - ({ container: result } = render( - serialization.getScriptReactElement({ content }) - )); + const { getScriptReactElement } = serialization; + ({ container: result } = render(getScriptReactElement({ content }))); }); it("should return a react element containing a JSON script tag, appropriately quoted by jsesc", () => { @@ -69,8 +69,9 @@ describe("serializationFactory", () => { describe("when a valid JSON string is passed", () => { beforeEach(() => { + const { getJSONFromScript } = serialization; document.body.innerHTML = ``; - result = serialization.getJSONFromScript(); + result = getJSONFromScript(); }); afterEach(() => { @@ -88,8 +89,9 @@ describe("serializationFactory", () => { describe("when an invalid JSON string is passed", () => { beforeEach(() => { + const { getJSONFromScript } = serialization; document.body.innerHTML = ``; - result = serialization.getJSONFromScript(); + result = getJSONFromScript(); }); it("should have logged a warning", () => { diff --git a/packages/webpack/.npmignore b/packages/webpack/.npmignore index 4238c07..67a89b0 100644 --- a/packages/webpack/.npmignore +++ b/packages/webpack/.npmignore @@ -1,5 +1,2 @@ * !lib/* -!docs/CHANGELOG.md -!docs/README.md -!package.json diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index 18853e4..f33559a 100644 --- a/packages/webpack/docs/CHANGELOG.md +++ b/packages/webpack/docs/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.9.0] - 2025-07-29 +## [0.9.0] - 2025-10-21 ### Changed @@ -19,7 +19,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - removed "next" peer dependency, this needn't be explicit - ensured files that cannot be resolved (by [enhanced-resolve](https://github.com/webpack/enhanced-resolve/)), for whatever reason, don't break the build +- don't try and filter potential resolutions, let enhanced-resolve try and potentially fail, to allow for resolve plugins to have irregular specifiers (e.g. path alias') - ensured that circular dependencies don't cause the module graph search lock up +- correct README.md to show `toggleHandler` as an option of the `pointCut`, not the general plugin configuration + +## [0.8.3] - 2025-09-30 + +### Fixed + +- Used the `prepare-publish.mjs` script provided by repo root version [0.12.0](../../../docs/CHANGELOG.md#0120---2025-09-30) to fix relative links in `README.md` on [npmjs.com](https://www.npmjs.com/) that were moved as part of [0.8.1](#081---2025-07-14) + +## [0.8.2] - 2025-09-29 + +### Fixed + +- No need for the `webpackNormalModule` parameter to be an async function, just needed to get static imports correct + +## [0.8.1] - 2025-07-14 + +### Fixed + +- ensured that `README.md` is included in the root of the npm package, to conform to [npmjs.org requirement](https://docs.npmjs.com/about-package-readme-files): +> An npm package `README.md` file **must** be in the root-level directory of the package. +- ensured that `LICENSE` is included in the npm package + +### Added + +- [keywords](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#keywords) added to the `package.json` to aid npm search ## [0.8.0] - 2025-05-27 diff --git a/packages/webpack/docs/README.md b/packages/webpack/docs/README.md index a262523..1a32ee0 100644 --- a/packages/webpack/docs/README.md +++ b/packages/webpack/docs/README.md @@ -32,19 +32,19 @@ Different code paths may have different toggling needs, and may want a toggle po The plugin constructor takes `TogglePointInjectionOptions` thus: ```typescript -import { NormalModule } from 'webpack'; +import webpack from 'webpack'; interface PointCut { name: string; togglePointModule: string; variantGlobs?: string[]; joinPointResolver?: (variantPath: string) => string; + toggleHandler?: string; } interface TogglePointInjectionOptions { pointCuts: PointCut[]; - toggleHandler?: string; - webpackNormalModule?: () => typeof NormalModule; + webpackNormalModule?: typeof webpack.NormalModule; } ``` @@ -131,10 +131,14 @@ e.g. a variant at `./__variants__/feature-name/variant-name/module.js` will reso #### _`webpackNormalModule`_ -If using [`Next.js`](https://nextjs.org/), an additional configuration parameter is needed, to inject the WebPack `NormalModule`, since the plugin is reliant on its hooks, and Next.js pre-compiles and packages Webpack, rather than referencing it as a normal package dependency. This can be specified thus (ESM): +If using [`Next.js`](https://nextjs.org/), an additional configuration parameter is needed, to inject the Webpack `NormalModule`, since the plugin is reliant on its hooks, and Next.js pre-compiles and packages Webpack, rather than referencing it as a normal package dependency. This can be specified thus (ESM): ```js -const webpackNormalModule = async () => (await import("next/dist/compiled/webpack/NormalModule.js")).default +import webpackNormalModule from "next/dist/compiled/webpack/NormalModule.js"; +const plugin = new TogglePointInjection({ + pointCuts, + webpackNormalModule +}); ``` ### How it Works diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 02afc72..d537c6d 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -12,6 +12,13 @@ }, "./pathSegmentToggleHandler": "./lib/pathSegmentToggleHandler.js" }, + "keywords": [ + "toggle point", + "feature toggles", + "feature flags", + "webpack", + "module proxy" + ], "repository": { "type": "git", "url": "git+https://github.com/asos/web-toggle-point.git", @@ -26,10 +33,11 @@ }, "scripts": { "build": "npm run clean && rollup -c build/rollup.mjs", - "prepublishOnly": "npm run build", + "prepublishOnly": "node ../prepare-publish.mjs && npm run build", + "postpublish": "shx rm ./README.md LICENSE", "clean": "rimraf ./lib", "docs": "rimraf ./docs/**/*.html && jsdoc -c ../jsdoc.conf.js", - "lint": "eslint build src docs --flag unstable_config_lookup_from_file", + "lint": "eslint build src docs --flag v10_config_lookup_from_file", "lint:fix": "npm run lint -- --fix", "test": "jest" }, @@ -54,7 +62,9 @@ "schema-utils": "^4.2.0", "webpack": "^5.88.2", "webpack-cli": "^4.10.0", - "webpack-test-utils": "^2.1.0" + "webpack-test-utils": "^2.1.0", + "shx": "^0.4.0", + "transform-markdown-links": "^2.1.0" }, "peerDependencies": { "webpack": ">=5.70" diff --git a/packages/webpack/src/plugins/togglePointInjection/index.js b/packages/webpack/src/plugins/togglePointInjection/index.js index c78aa96..9f10076 100644 --- a/packages/webpack/src/plugins/togglePointInjection/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/index.js @@ -57,7 +57,7 @@ class TogglePointInjection { fileSystem: compiler.inputFileSystem, options: this.options })); - NormalModule = await this.options.webpackNormalModule; + NormalModule = this.options.webpackNormalModule; }); compiler.hooks.compilation.tap( diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js index bfc049c..d6a9868 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/handleJoinPointMatch/resourceProxyExistsInRequestChain.js @@ -4,7 +4,7 @@ const resourceProxyExistsInRequestChain = ({ proxyResource }) => { const queue = [issuerModule]; - const visited = new Set(); + const visited = new WeakSet(); while (queue.length) { const node = queue.shift(); if (node.resource === proxyResource) { diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js index 40e24f2..21f3fb8 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.js @@ -4,9 +4,6 @@ import { promisify } from "util"; import handleJoinPointMatch from "./handleJoinPointMatch"; const { relative } = posix; -const isLoaderlessFileRequest = (request) => - [".", "/"].includes(request.at(0)) && !request.includes("!"); - const matchJoinPointIfResolved = async ({ enhancedResolve, resolveData, @@ -14,12 +11,18 @@ const matchJoinPointIfResolved = async ({ joinPointFiles, compilation }) => { - const resolved = await enhancedResolve( - {}, - resolveData.context, - resolveData.request, - {} - ); + let resolved; + try { + resolved = await enhancedResolve( + {}, + resolveData.context, + resolveData.request, + {} + ); + } catch { + return; + } + if (!resolved) { return; } @@ -50,8 +53,7 @@ const resolveJoinPoints = ({ async (resolveData) => { if ( !joinPointFiles.size || - !resolveData.context.replaceAll(sep, posix.sep).startsWith(appRoot) || - !isLoaderlessFileRequest(resolveData.request) + !resolveData.context.replaceAll(sep, posix.sep).startsWith(appRoot) ) { return; } diff --git a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js index b08d897..36a9c81 100644 --- a/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js +++ b/packages/webpack/src/plugins/togglePointInjection/resolveJoinPoints/index.test.js @@ -109,53 +109,40 @@ describe("resolveJoinPoints", () => { }); }); - describe("and the request is for a file with a webpack loader", () => { - beforeEach(() => { - resolveData = { - context: appRoot.replaceAll("/", sep), - request: "test-loader!test-request" - }; - beforeResolveCallback(resolveData); + const makeCommonAssertions = () => { + it("should resolve the request to a module file", () => { + expect(mockResolve).toHaveBeenCalledWith( + {}, + resolveData.context, + resolveData.request, + {}, + expect.any(Function) + ); }); + }; - it("should not try to handle a match", () => { - expect(handleJoinPointMatch).not.toHaveBeenCalled(); - }); - }); - - describe("and the request is for a module rather than a file", () => { + describe("and the request is for a file within the app root", () => { beforeEach(() => { resolveData = { context: appRoot.replaceAll("/", sep), - request: "test-request" + request: "/test-request" }; - beforeResolveCallback(resolveData); }); - it("should not try to handle a match", () => { - expect(handleJoinPointMatch).not.toHaveBeenCalled(); - }); - }); + describe("and an error is thrown whilst trying to resolve", () => { + beforeEach(() => { + mockResolve.mockImplementationOnce(() => { + throw new Error("Test error"); + }); + beforeResolveCallback(resolveData); + }); - describe("and the request is for a file within the app root, without any loaders", () => { - beforeEach(() => { - resolveData = { - context: appRoot.replaceAll("/", sep), - request: "/test-request" - }; - }); + makeCommonAssertions(); - const makeCommonAssertions = () => { - it("should resolve the request to a module file", () => { - expect(mockResolve).toHaveBeenCalledWith( - {}, - resolveData.context, - resolveData.request, - {}, - expect.any(Function) - ); + it("should not try to handle a match", () => { + expect(handleJoinPointMatch).not.toHaveBeenCalled(); }); - }; + }); describe("and the file cannot be resolved", () => { beforeEach(() => { diff --git a/peripheral/babel-preset-asos/package.json b/peripheral/babel-preset-asos/package.json index ca4d000..46a214d 100644 --- a/peripheral/babel-preset-asos/package.json +++ b/peripheral/babel-preset-asos/package.json @@ -3,10 +3,6 @@ "version": "0.5.34", "author": "ASOS Web Team ", "private": true, - "keywords": [ - "asos", - "babel" - ], "engines": { "node": ">=18" }, diff --git a/peripheral/browserslist-config-asos/package.json b/peripheral/browserslist-config-asos/package.json index 29333aa..2f3b0cf 100644 --- a/peripheral/browserslist-config-asos/package.json +++ b/peripheral/browserslist-config-asos/package.json @@ -3,10 +3,6 @@ "version": "0.5.34", "author": "ASOS Web Team ", "private": true, - "keywords": [ - "asos", - "browserslist" - ], "engines": { "node": ">=18" }, diff --git a/test/automation/docs/CHANGELOG.md b/test/automation/docs/CHANGELOG.md index add0c91..a3b3a70 100644 --- a/test/automation/docs/CHANGELOG.md +++ b/test/automation/docs/CHANGELOG.md @@ -5,9 +5,15 @@ 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.1.3] - 2025-08-19 +## [0.1.4] - 2025-09-29 -## Changed +### Fixed + +- removed `keywords` from `package.json`, since serving no purpose in a private package + +## [0.1.3] - 2025-08-21 + +### Changed - move to import type for `ReporterDescription` diff --git a/test/automation/package.json b/test/automation/package.json index 70dd3cb..c25066a 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -1,8 +1,7 @@ { "name": "web-toggle-point-automation-tests", - "version": "0.1.3", - "main": "index.js", - "keywords": [], + "version": "0.1.4", + "main": "index.js", "type": "module", "private": true, "scripts": { @@ -17,7 +16,7 @@ }, "description": "", "devDependencies": { - "@playwright/test": "^1.49.0", + "@playwright/test": "^1.56.0", "@types/node": "^22.9.1", "path-exists-cli": "^2.0.0" } From 4c2060c72fb8e2b7758db60634224f75602ac569 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:18:11 +0100 Subject: [PATCH 28/44] revert workspaces lint change upgrade eslint --- docs/CHANGELOG.md | 1 + examples/next/playwright.config.ts | 1 + package-lock.json | 4 +++- package.json | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3b8b933..af06760 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,7 @@ This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). - updated to `0.11.0` of [`eslint-plugin-workspaces`](https://github.com/joshuajaco/eslint-plugin-workspaces) after [addition of ESLint9 support](https://github.com/joshuajaco/eslint-plugin-workspaces/commit/af855c3a3d8069366d4446747e91828ddf7560c6) - update `eslint.config.mjs` to utilise flat config +- updated `eslint` to 9.38.0 ## [0.12.0] - 2025-09-30 diff --git a/examples/next/playwright.config.ts b/examples/next/playwright.config.ts index 88d6b5e..331c13c 100644 --- a/examples/next/playwright.config.ts +++ b/examples/next/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, type PlaywrightTestConfig } from "@playwright/test"; +// eslint-disable-next-line workspaces/no-relative-imports, workspaces/require-dependency import baseConfig from "../../test/automation/base.config"; const THREE_MINUTES = 3 * 60 * 1000; diff --git a/package-lock.json b/package-lock.json index 40af798..02810f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "core-js": "^3.0.0", "cross-env": "^7.0.3", "danger": "^13.0.3", - "eslint": "^9.15.0", + "eslint": "^9.38.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.9.0", "eslint-plugin-jest-formatting": "^3.1.0", @@ -7899,6 +7899,8 @@ }, "node_modules/eslint": { "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", diff --git a/package.json b/package.json index 7ceab75..3459e55 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "core-js": "^3.0.0", "cross-env": "^7.0.3", "danger": "^13.0.3", - "eslint": "^9.15.0", + "eslint": "^9.38.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.9.0", "eslint-plugin-jest-formatting": "^3.1.0", From 8aa52e4b3cb2dcc685c06696fa056106c1f213c4 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:21:19 +0100 Subject: [PATCH 29/44] revert toVisible change to reduce noise --- .../express/src/routes/animals/playwright.spec.js | 2 +- .../express/src/routes/config/playwright.spec.js | 6 ++++-- .../1-varied-component/playwright.spec.ts | 4 ++-- .../playwright.spec.ts | 4 ++-- .../experiments/4-varied-variant/playwright.spec.ts | 6 +++--- .../5-toggle-config-opt-out/playwright.spec.ts | 4 ++-- .../playwright.spec.ts | 8 ++++---- .../experiments/8-toggled-twice/playwright.spec.ts | 12 ++++++------ .../serve/src/fixtures/audience/playwright.spec.ts | 4 ++-- .../serve/src/fixtures/config/playwright.spec.ts | 8 ++++---- .../src/fixtures/translation/playwright.spec.ts | 4 ++-- 11 files changed, 32 insertions(+), 30 deletions(-) diff --git a/examples/express/src/routes/animals/playwright.spec.js b/examples/express/src/routes/animals/playwright.spec.js index 8a8b0b6..7cadd10 100644 --- a/examples/express/src/routes/animals/playwright.spec.js +++ b/examples/express/src/routes/animals/playwright.spec.js @@ -38,7 +38,7 @@ test.describe("versioned image endpoint", () => { await page.goto("http://localhost:3002/animals"); await expect( page.locator(`img[data-version="${version}"]`) - ).toBeVisible(); + ).toBeInViewport(); }); }); }); diff --git a/examples/express/src/routes/config/playwright.spec.js b/examples/express/src/routes/config/playwright.spec.js index edb4a71..94e49ec 100644 --- a/examples/express/src/routes/config/playwright.spec.js +++ b/examples/express/src/routes/config/playwright.spec.js @@ -14,7 +14,7 @@ test.describe("config endpoint", () => { }); test("it shows a varied experience", async ({ page }) => { - await expect(page.getByText(`I am ${size}`)).toBeVisible(); + await expect(page.getByText(`I am ${size}`)).toBeInViewport(); }); scenarios @@ -25,7 +25,9 @@ test.describe("config endpoint", () => { page }) => { await page.getByRole("button", { name: otherSize }).click(); - await expect(page.getByText(`I am ${otherSize}`)).toBeVisible(); + await expect( + page.getByText(`I am ${otherSize}`) + ).toBeInViewport(); }); }); }); diff --git a/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts index 0784359..be4fe6b 100644 --- a/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts +++ b/examples/next/src/app/fixtures/experiments/1-varied-component/playwright.spec.ts @@ -8,7 +8,7 @@ test.describe("varying a component", () => { test.describe("when no experiments header set", () => { test("it shows a default experience", async ({ page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "control 1")).toBeVisible(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); }); }); @@ -23,7 +23,7 @@ test.describe("varying a component", () => { }); test("it shows a varied experience", async ({ page }) => { - await expect(locateWithinExample(page, "variant 1")).toBeVisible(); + await expect(locateWithinExample(page, "variant 1")).toBeInViewport(); }); test("it should activate the feature with the toggle router", async () => { diff --git a/examples/next/src/app/fixtures/experiments/3-varied-component-extending-control/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/3-varied-component-extending-control/playwright.spec.ts index 405bab1..aa284db 100644 --- a/examples/next/src/app/fixtures/experiments/3-varied-component-extending-control/playwright.spec.ts +++ b/examples/next/src/app/fixtures/experiments/3-varied-component-extending-control/playwright.spec.ts @@ -8,7 +8,7 @@ test.describe("varying a component that imports and extends the control componen test.describe("when no experiments header set", () => { test("it shows a default experience", async ({ page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "control 1")).toBeVisible(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); await expect( locateWithinExample(page, "control 1").locator("..") ).not.toContainText("Variant 1"); @@ -22,7 +22,7 @@ test.describe("varying a component that imports and extends the control componen page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "control 1")).toBeVisible(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); await expect( locateWithinExample(page, "control 1").locator("..") ).toContainText("Variant 1"); diff --git a/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts index 09909a9..5fbee00 100644 --- a/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts +++ b/examples/next/src/app/fixtures/experiments/4-varied-variant/playwright.spec.ts @@ -8,7 +8,7 @@ test.describe("varying a variant in a second experiment", () => { test.describe("when no experiments header set", () => { test("it shows a default experience", async ({ page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "control 1")).toBeVisible(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); }); }); @@ -23,7 +23,7 @@ test.describe("varying a variant in a second experiment", () => { }); test("it shows a varied experience", async ({ page }) => { - await expect(locateWithinExample(page, "variant 1")).toBeVisible(); + await expect(locateWithinExample(page, "variant 1")).toBeInViewport(); }); test("it should activate the feature with the toggle router", async () => { @@ -39,7 +39,7 @@ test.describe("varying a variant in a second experiment", () => { }); test("it shows a doubly-varied experience", async ({ page }) => { - await expect(locateWithinExample(page, "variant 2")).toBeVisible(); + await expect(locateWithinExample(page, "variant 2")).toBeInViewport(); }); test("it should activate the second feature with the toggle router", async () => { diff --git a/examples/next/src/app/fixtures/experiments/5-toggle-config-opt-out/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/5-toggle-config-opt-out/playwright.spec.ts index 3aace00..209929c 100644 --- a/examples/next/src/app/fixtures/experiments/5-toggle-config-opt-out/playwright.spec.ts +++ b/examples/next/src/app/fixtures/experiments/5-toggle-config-opt-out/playwright.spec.ts @@ -8,7 +8,7 @@ test.describe("preventing toggling of a module using a toggleConfig", () => { 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(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); }); }); @@ -19,7 +19,7 @@ test.describe("preventing toggling of a module using a toggleConfig", () => { page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "control 1")).toBeVisible(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); }); }); }); diff --git a/examples/next/src/app/fixtures/experiments/6-toggle-config-variant-filter-same-directory/playwright.spec.ts b/examples/next/src/app/fixtures/experiments/6-toggle-config-variant-filter-same-directory/playwright.spec.ts index d7535c1..8e61b51 100644 --- a/examples/next/src/app/fixtures/experiments/6-toggle-config-variant-filter-same-directory/playwright.spec.ts +++ b/examples/next/src/app/fixtures/experiments/6-toggle-config-variant-filter-same-directory/playwright.spec.ts @@ -8,8 +8,8 @@ test.describe("varying a component whilst using a toggle config to control the j test.describe("when no experiments header set", () => { test("it shows two control modules", async ({ page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "control 1")).toBeVisible(); - await expect(locateWithinExample(page, "control 2")).toBeVisible(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); + await expect(locateWithinExample(page, "control 2")).toBeInViewport(); }); }); @@ -20,8 +20,8 @@ test.describe("varying a component whilst using a toggle config to control the j page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "variant 1")).toBeVisible(); - await expect(locateWithinExample(page, "control 2")).toBeVisible(); + await expect(locateWithinExample(page, "variant 1")).toBeInViewport(); + await expect(locateWithinExample(page, "control 2")).toBeInViewport(); }); }); }); 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 index 8a24ab0..8af7c0b 100644 --- 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 @@ -8,7 +8,7 @@ test.describe("varying a component and varying again with a different toggle poi 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(); + await expect(locateWithinExample(page, "control 1")).toBeInViewport(); }); }); @@ -17,7 +17,7 @@ test.describe("varying a component and varying again with a different toggle poi test("it shows a varied module", async ({ page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "variant 1")).toBeVisible(); + await expect(locateWithinExample(page, "variant 1")).toBeInViewport(); }); test.describe("when the 'a' key is pressed", () => { @@ -27,10 +27,10 @@ test.describe("varying a component and varying again with a different toggle poi await page.goto(fixtureURL); await page.waitForTimeout(50); await page.keyboard.down("a"); - await expect(locateWithinExample(page, "variant 1")).toBeVisible(); + await expect(locateWithinExample(page, "variant 1")).toBeInViewport(); await expect( page.locator("p", { hasText: "pressed: a" }) - ).toBeVisible(); + ).toBeInViewport(); }); }); @@ -41,10 +41,10 @@ test.describe("varying a component and varying again with a different toggle poi await page.goto(fixtureURL); await page.waitForTimeout(50); await page.keyboard.down("n"); - await expect(locateWithinExample(page, "variant 2")).toBeVisible(); + await expect(locateWithinExample(page, "variant 2")).toBeInViewport(); await expect( page.locator("p", { hasText: "pressed: n" }) - ).toBeVisible(); + ).toBeInViewport(); }); }); }); diff --git a/examples/serve/src/fixtures/audience/playwright.spec.ts b/examples/serve/src/fixtures/audience/playwright.spec.ts index 4e855c9..3f99fa3 100644 --- a/examples/serve/src/fixtures/audience/playwright.spec.ts +++ b/examples/serve/src/fixtures/audience/playwright.spec.ts @@ -8,7 +8,7 @@ test.describe("audience-specific content", () => { page.getByText( "Some audience-specific content: this is a value for the default experience" ) - ).toBeVisible(); + ).toBeInViewport(); }); }); @@ -27,7 +27,7 @@ test.describe("audience-specific content", () => { await page.goto("/"); await expect( page.getByText(`Some audience-specific content: ${expected}`) - ).toBeVisible(); + ).toBeInViewport(); }); }); }); diff --git a/examples/serve/src/fixtures/config/playwright.spec.ts b/examples/serve/src/fixtures/config/playwright.spec.ts index 91b7ebf..1bcd17e 100644 --- a/examples/serve/src/fixtures/config/playwright.spec.ts +++ b/examples/serve/src/fixtures/config/playwright.spec.ts @@ -6,12 +6,12 @@ test.describe("site-specific content", () => { await page.goto("/"); await expect( page.getByText("Some site-specific content: this is a default value") - ).toBeVisible(); + ).toBeInViewport(); await expect( page.getByText( "Some more site-specific content: this is a default value from subpath" ) - ).toBeVisible; + ).toBeInViewport; }); }); @@ -50,10 +50,10 @@ test.describe("site-specific content", () => { await page.goto(`/${site}`); await expect( page.getByText(`Some site-specific content: ${expected1}`) - ).toBeVisible(); + ).toBeInViewport(); await expect( page.getByText(`Some more site-specific content: ${expected2}`) - ).toBeVisible(); + ).toBeInViewport(); }); }); }); diff --git a/examples/serve/src/fixtures/translation/playwright.spec.ts b/examples/serve/src/fixtures/translation/playwright.spec.ts index 181ec2c..cb79dc6 100644 --- a/examples/serve/src/fixtures/translation/playwright.spec.ts +++ b/examples/serve/src/fixtures/translation/playwright.spec.ts @@ -6,7 +6,7 @@ test.describe("translating content", () => { await page.goto("/"); await expect( page.getByText("Some translated content: english value") - ).toBeVisible(); + ).toBeInViewport(); }); }); @@ -20,7 +20,7 @@ test.describe("translating content", () => { await page.goto("/"); await expect( page.getByText(`Some translated content: ${expected}`) - ).toBeVisible(); + ).toBeInViewport(); }); }); }); From 627e7c46d43c07948bd63468904c682da9284c56 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:24:06 +0100 Subject: [PATCH 30/44] EOL --- .../components/TopBox/TopBoxChild/TopBoxButton/index.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css index 3bf108e..6b4e540 100644 --- a/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css +++ b/examples/express/src/routes/parallel-folder-convention/components/TopBox/TopBoxChild/TopBoxButton/index.module.css @@ -3,4 +3,4 @@ border: 1px solid black; border-radius: 20%; font-size: xxx-large; -} \ No newline at end of file +} From f6891e6f5a9d9051a1f8ce86705a81368edb32ef Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:01:31 +0100 Subject: [PATCH 31/44] changelogs for eslint change --- examples/next/docs/CHANGELOG.md | 3 ++- examples/serve/docs/CHANGELOG.md | 7 ++++++- packages/features/docs/CHANGELOG.md | 6 ++++++ packages/features/package.json | 2 +- packages/react-pointcuts/docs/CHANGELOG.md | 7 +++++++ packages/react-pointcuts/package.json | 2 +- packages/ssr/docs/CHANGELOG.md | 6 ++++++ packages/ssr/package.json | 2 +- packages/webpack/docs/CHANGELOG.md | 6 ++++++ packages/webpack/package.json | 2 +- test/automation/docs/CHANGELOG.md | 6 ++++++ test/automation/package.json | 2 +- 12 files changed, 44 insertions(+), 7 deletions(-) diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index 909ed70..b6e7bfe 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - update to take supply static `webpackNormalModule` corresponding to webpack plugin [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) -- update NextJs to version 15.5.6 +- update [`Next.js`](https://nextjs.org/) to version 15.5.6 +- update [`@playwright/test`](https://github.com/microsoft/playwright/tree/main/packages/playwright-test) to 1.56.0 ## [0.4.0] - 2025-10-20 diff --git a/examples/serve/docs/CHANGELOG.md b/examples/serve/docs/CHANGELOG.md index ad96a31..ecb3b6c 100644 --- a/examples/serve/docs/CHANGELOG.md +++ b/examples/serve/docs/CHANGELOG.md @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.0] - 2025-10-21 -### Changed +### Added - updated toggle handlers to take a `variantPathMap` corresponding to webpack [version 0.9.0](../../../packages/webpack/docs/CHANGELOG.md#090---2025-07-29) +- `@typescript-eslint/parser` to support the above + +### Fixed + +- convert deprecated `unstable_config_lookup_from_file` eslint flag to `v10_config_lookup_from_file` now that [its stable](https://eslint.org/docs/latest/flags/#active-flags) awaiting next major version ## [0.3.0] - 2025-10-20 diff --git a/packages/features/docs/CHANGELOG.md b/packages/features/docs/CHANGELOG.md index f7baa9e..720d815 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.5.1] - 2025-10-21 + +### Fixed + +- convert deprecated `unstable_config_lookup_from_file` eslint flag to `v10_config_lookup_from_file` now that [its stable](https://eslint.org/docs/latest/flags/#active-flags) awaiting next major version + ## [0.5.0] - 2025-10-20 ### Changed diff --git a/packages/features/package.json b/packages/features/package.json index a4f38e7..087fb41 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, used to store toggle state", - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "type": "module", "main": "./lib/global.js", diff --git a/packages/react-pointcuts/docs/CHANGELOG.md b/packages/react-pointcuts/docs/CHANGELOG.md index d6c3303..ad11a1c 100644 --- a/packages/react-pointcuts/docs/CHANGELOG.md +++ b/packages/react-pointcuts/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.5.1] - 2025-10-21 + +### Fixed + +- convert deprecated `unstable_config_lookup_from_file` eslint flag to `v10_config_lookup_from_file` now that [its stable](https://eslint.org/docs/latest/flags/#active-flags) awaiting next major version +- reverted errant linebreak + ## [0.5.0] - 2025-09-30 ### Changed diff --git a/packages/react-pointcuts/package.json b/packages/react-pointcuts/package.json index 0258b63..0624330 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.5.0", + "version": "0.5.1", "license": "MIT", "type": "module", "main": "./lib/main.es5.cjs", diff --git a/packages/ssr/docs/CHANGELOG.md b/packages/ssr/docs/CHANGELOG.md index a6dc120..cf97136 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.7] - 2025-10-21 + +### Fixed + +- convert deprecated `unstable_config_lookup_from_file` eslint flag to `v10_config_lookup_from_file` now that [its stable](https://eslint.org/docs/latest/flags/#active-flags) awaiting next major version + ## [0.2.6] - 2025-09-30 ### Fixed diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 5fc9221..0e023a9 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.6", + "version": "0.2.7", "license": "MIT", "type": "module", "main": "./lib/main.es5.cjs", diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index f33559a..cf0729a 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.9.1] - 2025-10-21 + +### Fixed + +- convert deprecated `unstable_config_lookup_from_file` eslint flag to `v10_config_lookup_from_file` now that [its stable](https://eslint.org/docs/latest/flags/#active-flags) awaiting next major version + ## [0.9.0] - 2025-10-21 ### Changed diff --git a/packages/webpack/package.json b/packages/webpack/package.json index d537c6d..b7c4c53 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.9.0", + "version": "0.9.1", "license": "MIT", "type": "module", "main": "./lib/main.cjs", diff --git a/test/automation/docs/CHANGELOG.md b/test/automation/docs/CHANGELOG.md index a3b3a70..2af2dbf 100644 --- a/test/automation/docs/CHANGELOG.md +++ b/test/automation/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.1.5] - 2025-10-21 + +### Changed + +- update [`@playwright/test`](https://github.com/microsoft/playwright/tree/main/packages/playwright-test) to 1.56.0 + ## [0.1.4] - 2025-09-29 ### Fixed diff --git a/test/automation/package.json b/test/automation/package.json index c25066a..d9725e2 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -1,6 +1,6 @@ { "name": "web-toggle-point-automation-tests", - "version": "0.1.4", + "version": "0.1.5", "main": "index.js", "type": "module", "private": true, From ccef8eb47019634f602bc7732cdb11df5a02dcee Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:03:25 +0100 Subject: [PATCH 32/44] revert toBeVisible change to reduce noise --- .../experiments/8-toggled-twice/playwright.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index 8af7c0b..8a24ab0 100644 --- 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 @@ -8,7 +8,7 @@ test.describe("varying a component and varying again with a different toggle poi 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")).toBeInViewport(); + await expect(locateWithinExample(page, "control 1")).toBeVisible(); }); }); @@ -17,7 +17,7 @@ test.describe("varying a component and varying again with a different toggle poi test("it shows a varied module", async ({ page }) => { await page.goto(fixtureURL); - await expect(locateWithinExample(page, "variant 1")).toBeInViewport(); + await expect(locateWithinExample(page, "variant 1")).toBeVisible(); }); test.describe("when the 'a' key is pressed", () => { @@ -27,10 +27,10 @@ test.describe("varying a component and varying again with a different toggle poi await page.goto(fixtureURL); await page.waitForTimeout(50); await page.keyboard.down("a"); - await expect(locateWithinExample(page, "variant 1")).toBeInViewport(); + await expect(locateWithinExample(page, "variant 1")).toBeVisible(); await expect( page.locator("p", { hasText: "pressed: a" }) - ).toBeInViewport(); + ).toBeVisible(); }); }); @@ -41,10 +41,10 @@ test.describe("varying a component and varying again with a different toggle poi await page.goto(fixtureURL); await page.waitForTimeout(50); await page.keyboard.down("n"); - await expect(locateWithinExample(page, "variant 2")).toBeInViewport(); + await expect(locateWithinExample(page, "variant 2")).toBeVisible(); await expect( page.locator("p", { hasText: "pressed: n" }) - ).toBeInViewport(); + ).toBeVisible(); }); }); }); From 84df310bc9b787d6b09ab416be434eb1c5aa6ef1 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:04:49 +0100 Subject: [PATCH 33/44] more toBeVisible noise reduction --- examples/express/src/routes/config/playwright.spec.js | 6 ++---- examples/serve/src/fixtures/audience/playwright.spec.ts | 4 ++-- examples/serve/src/fixtures/translation/playwright.spec.ts | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/express/src/routes/config/playwright.spec.js b/examples/express/src/routes/config/playwright.spec.js index 94e49ec..edb4a71 100644 --- a/examples/express/src/routes/config/playwright.spec.js +++ b/examples/express/src/routes/config/playwright.spec.js @@ -14,7 +14,7 @@ test.describe("config endpoint", () => { }); test("it shows a varied experience", async ({ page }) => { - await expect(page.getByText(`I am ${size}`)).toBeInViewport(); + await expect(page.getByText(`I am ${size}`)).toBeVisible(); }); scenarios @@ -25,9 +25,7 @@ test.describe("config endpoint", () => { page }) => { await page.getByRole("button", { name: otherSize }).click(); - await expect( - page.getByText(`I am ${otherSize}`) - ).toBeInViewport(); + await expect(page.getByText(`I am ${otherSize}`)).toBeVisible(); }); }); }); diff --git a/examples/serve/src/fixtures/audience/playwright.spec.ts b/examples/serve/src/fixtures/audience/playwright.spec.ts index 3f99fa3..4e855c9 100644 --- a/examples/serve/src/fixtures/audience/playwright.spec.ts +++ b/examples/serve/src/fixtures/audience/playwright.spec.ts @@ -8,7 +8,7 @@ test.describe("audience-specific content", () => { page.getByText( "Some audience-specific content: this is a value for the default experience" ) - ).toBeInViewport(); + ).toBeVisible(); }); }); @@ -27,7 +27,7 @@ test.describe("audience-specific content", () => { await page.goto("/"); await expect( page.getByText(`Some audience-specific content: ${expected}`) - ).toBeInViewport(); + ).toBeVisible(); }); }); }); diff --git a/examples/serve/src/fixtures/translation/playwright.spec.ts b/examples/serve/src/fixtures/translation/playwright.spec.ts index cb79dc6..181ec2c 100644 --- a/examples/serve/src/fixtures/translation/playwright.spec.ts +++ b/examples/serve/src/fixtures/translation/playwright.spec.ts @@ -6,7 +6,7 @@ test.describe("translating content", () => { await page.goto("/"); await expect( page.getByText("Some translated content: english value") - ).toBeInViewport(); + ).toBeVisible(); }); }); @@ -20,7 +20,7 @@ test.describe("translating content", () => { await page.goto("/"); await expect( page.getByText(`Some translated content: ${expected}`) - ).toBeInViewport(); + ).toBeVisible(); }); }); }); From 5926db1ac6caee3555e80e66c45576c218a1d48c Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:06:10 +0100 Subject: [PATCH 34/44] more toBeVisible stuff --- examples/serve/src/fixtures/config/playwright.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/serve/src/fixtures/config/playwright.spec.ts b/examples/serve/src/fixtures/config/playwright.spec.ts index 1bcd17e..91b7ebf 100644 --- a/examples/serve/src/fixtures/config/playwright.spec.ts +++ b/examples/serve/src/fixtures/config/playwright.spec.ts @@ -6,12 +6,12 @@ test.describe("site-specific content", () => { await page.goto("/"); await expect( page.getByText("Some site-specific content: this is a default value") - ).toBeInViewport(); + ).toBeVisible(); await expect( page.getByText( "Some more site-specific content: this is a default value from subpath" ) - ).toBeInViewport; + ).toBeVisible; }); }); @@ -50,10 +50,10 @@ test.describe("site-specific content", () => { await page.goto(`/${site}`); await expect( page.getByText(`Some site-specific content: ${expected1}`) - ).toBeInViewport(); + ).toBeVisible(); await expect( page.getByText(`Some more site-specific content: ${expected2}`) - ).toBeInViewport(); + ).toBeVisible(); }); }); }); From 2a831e916bf5f6e2bb15888ac4c7ad20326df64e Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:19:07 +0100 Subject: [PATCH 35/44] empty commit to kick off CI From d1bc3a191a31e6275ee3bcf039aa71d6edcd7499 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:27:33 +0100 Subject: [PATCH 36/44] british spelling. consistent punctuation. --- .../express/src/routes/parallel-folder-convention/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md index d344d5f..9e0e08d 100644 --- a/examples/express/src/routes/parallel-folder-convention/README.md +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -51,7 +51,7 @@ The features comprise the following: ### _baseline_ -The base experience. Clicking the dog dispatches a redux action adding a dog to the bottom box +The base experience. Clicking the dog dispatches a redux action adding a dog to the bottom box. ### _feature1_ @@ -86,7 +86,7 @@ Varied constant (`constants/index.ts`), react component (`TopBoxChild`) and redu ### _feature3_ -Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with modified reducer action that multiplies rabbits, when added +Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with modified reducer action that multiplies rabbits, when added. ```bash ├── constants @@ -99,7 +99,7 @@ Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with modified ### _feature4_ -Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with replaced redux selector that carcinizes previously added animals +Varied constant (`constants/index.ts`) & redux slice (`slice.ts`) with replaced redux selector that carcinises previously added animals. ```bash ├── constants From 75a58291e11ef988dd3768ac5e5045be341a88c4 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:54:52 +0100 Subject: [PATCH 37/44] update docs for variantPathMap (missed in last update) --- packages/webpack/docs/CHANGELOG.md | 6 ++++++ packages/webpack/docs/README.md | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index cf0729a..535ebea 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.9.2] - 2025-10-22 + +### Fixed + +- updated the documentation to align with move to `variantPathMap`, missed in [version 0.9.0](#090---2025-10-21) + ## [0.9.1] - 2025-10-21 ### Fixed diff --git a/packages/webpack/docs/README.md b/packages/webpack/docs/README.md index 1a32ee0..1a75e68 100644 --- a/packages/webpack/docs/README.md +++ b/packages/webpack/docs/README.md @@ -99,7 +99,7 @@ If not supplied, a default `glob` of `/**/__variants__/*/*/!(*.test).{js,jsx,ts, #### _`toggleHandler`_ -This module unpicks the [WebPack context module](https://webpack.js.org/guides/dependency-management/#context-module-api) produced by enacting the configured `variantGlobs` and converts it into a form suitable for the configured `togglePoint`. +This module unpicks a `variantPathMap` (a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) of full paths to [module namespace objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#module_namespace_object)) produced by enacting the configured `variantGlobs` 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 `variantGlobs`, 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. @@ -176,9 +176,9 @@ const plugin = new TogglePointInjection({ ...the plugin inject a proxy module with the id `toggle:/join-points:/src/modules/myModule.js` into the compilation, to which all requests for `/src/modules/myModule.js` will be redirected.[^4] -That proxy module will, in turn, import a module with id `toggle:/point-cuts:/my point cut`, and pass it the original module for `/src/modules/myModule.js` as a `pointCut` argument, plus all the possible variation modules (`./feature1/variant1/myModule.js`, `./feature2/variant1/myModule.js`, `./feature2/variant2/myModule.js`) in a [WebPack context module](https://webpack.js.org/guides/dependency-management/#context-module-api). +That proxy module will, in turn, import a module with id `toggle:/point-cuts:/my point cut`, and pass it the original module for `/src/modules/myModule.js` as a `pointCut` argument, plus all the possible variation modules (`./feature1/variant1/myModule.js`, `./feature2/variant1/myModule.js`, `./feature2/variant2/myModule.js`) in a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) linking the full paths to the corresponding [module namespace objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#module_namespace_object). -The `toggle:/point-cuts:/my point cut` then imports the configured toggle point (and toggle handler, if configured), then calls the handler with the toggle point, join point module, and variants. The handler is expected to unpick the webpack-specific context module, and mutate it into something consumable by the toggle point. +The `toggle:/point-cuts:/my point cut` then imports the configured toggle point (and toggle handler, if configured), then calls the handler with the toggle point, join point module, and variants. The handler is expected to convert the key/value `Map` of variants into a data structure appropriate for the toggle point (the default being a `Map` keyed by feature, then variant, to variant module). This toggle point is then expected to return the outcome, having chosen the appropriate module at runtime. From baf68c4460a15fe26502e8a29f3b6d843254d696 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:10:54 +0100 Subject: [PATCH 38/44] add playwright spec --- .../playwright.spec.js | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 examples/express/src/routes/parallel-folder-convention/playwright.spec.js diff --git a/examples/express/src/routes/parallel-folder-convention/playwright.spec.js b/examples/express/src/routes/parallel-folder-convention/playwright.spec.js new file mode 100644 index 0000000..30f05ff --- /dev/null +++ b/examples/express/src/routes/parallel-folder-convention/playwright.spec.js @@ -0,0 +1,201 @@ +import { test, expect } from "@playwright/test"; + +test.describe("parallel folder convention endpoint", () => { + test.describe("with no version header", () => { + test.beforeEach(async ({ page }) => { + await page.goto("http://localhost:3002/parallel-folder-convention/"); + }); + + test("it shows the control experience", async ({ page }) => { + const control = page.getByTestId("control 1"); + await expect(control).toBeAttached(); + await expect(control.getByText("🐶")).toBeAttached(); + await expect(control.getByTestId("control 2")).toBeAttached(); + await expect( + page.locator(':nth-match(:text("Control 2"), 2)') + ).toBeAttached(); + }); + + test.describe("when clicking on the dog button", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("button").click(); + }); + + test("it adds a dog to the lower container", async ({ page }) => { + await page.getByRole("button").click(); + await expect( + page.locator(':nth-match(:text("Control 2"), 2) :text("🐶")') + ).toBeAttached(); + }); + + test.describe("when changing the feature to 'feature 1'", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("combobox").selectOption("feature 1 🐱"); + }); + + test("it shows a varied experience", async ({ page }) => { + const control = page.getByTestId("variant 1"); + await expect(control).toBeAttached(); + await expect(control.getByText("🐱")).toBeAttached(); + await expect(control.getByText("🐱")).toHaveCSS( + "background-color", + "rgb(245, 222, 179)" + ); + await expect(page.getByTestId("variant 2")).toBeAttached(); + }); + + test("it preserves the state", async ({ page }) => { + await expect( + page.locator(':text("Variant 2") :text("🐶")') + ).toBeAttached(); + }); + + test.describe("when clicking on the cat button", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("button").click(); + }); + + test("it adds a cat to the lower container", async ({ page }) => { + await expect( + page.locator(':text("Variant 2") :text("🐱")') + ).toBeAttached(); + }); + + test.describe("when changing the feature to 'feature 2'", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("combobox").selectOption("feature 2 🐹"); + }); + + test("it shows a different varied experience", async ({ page }) => { + const control = page.getByTestId("control 1"); + await expect(control).toBeAttached(); + await expect(control.getByTestId("control 2")).toBeAttached(); + await expect(control.getByText("🐹")).toBeAttached(); + await expect(control.getByText("⛓️‍💥")).toBeAttached(); + await expect( + page.locator(':nth-match(:text("Control 2"), 2)') + ).toBeAttached(); + }); + + test("it preserves the state", async ({ page }) => { + await expect( + page.locator(':text("Control 2") :text("🐶🐱")') + ).toBeAttached(); + }); + + test.describe("when clicking the 'free the animals' button", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("button", { name: "⛓️‍💥" }).click(); + }); + + test("it clears the state", async ({ page }) => { + await expect( + page.locator(':nth-match(:text("Control 2"), 2)') + ).toHaveText("Control 2"); + }); + }); + }); + }); + }); + }); + }); + + test.describe("when a 'feature2' feature header is sent with the request", () => { + test.beforeEach(async ({ page }) => { + await page.setExtraHTTPHeaders({ feature: "feature2" }); + await page.goto("http://localhost:3002/parallel-folder-convention/"); + }); + + test("it shows the varied experience (default state with two hamsters)", async ({ + page + }) => { + const control = page.getByTestId("control 1"); + await expect(control).toBeAttached(); + await expect(control.getByText("🐹")).toBeAttached(); + await expect( + page.locator(':nth-match(:text("Control 2"), 2) :text("🐹🐹")') + ).toBeAttached(); + }); + + test.describe("when changing the feature to 'feature 5'", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("combobox").selectOption("feature 5 🪐"); + }); + + test("shows the varied experience (new store & slice)", async ({ + page + }) => { + const control = page.getByTestId("variant 2"); + await expect(control).toBeAttached(); + await expect(control.getByText("🌎🪐☄️🛸")).toBeAttached(); + }); + + test("it maintains the previous state (two hamsters shown before)", async ({ + page + }) => { + await expect( + page.locator(':text("Control 2") :text("🐹🐹")') + ).toBeAttached(); + }); + }); + }); + + test.describe("when a 'feature3' feature header is sent with the request", () => { + test.beforeEach(async ({ page }) => { + await page.setExtraHTTPHeaders({ feature: "feature3" }); + await page.goto("http://localhost:3002/parallel-folder-convention/"); + }); + + test("it shows the varied experience", async ({ page }) => { + const control = page.getByTestId("control 1"); + await expect(control).toBeAttached(); + await expect(control.getByText("🐰")).toBeAttached(); + }); + + test.describe("when clicking on the rabbit button", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("button").click(); + }); + + test("it adds a rabbit to the container", async ({ page }) => { + await expect( + page.locator(':nth-match(:text("Control 2"), 2) :text("🐰")') + ).toBeAttached(); + }); + + test.describe("when clicking on the rabbit button another two times", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("button").click(); + await page.getByRole("button").click(); + }); + + test("it adds three more rabbits to the container (multiplying redux action)", async ({ + page + }) => { + await expect( + page.locator(':nth-match(:text("Control 2"), 2) :text("🐰🐰🐰🐰")') + ).toBeAttached(); + }); + + test.describe("when changing the feature to 'feature 4'", () => { + test.beforeEach(async ({ page }) => { + await page.getByRole("combobox").selectOption("feature 4 🦀"); + }); + + test("it shows the varied experience (selector that modifies existing state)", async ({ + page + }) => { + const control = page.getByTestId("control 1"); + await expect(control).toBeAttached(); + await expect(control.getByText("🦀")).toBeAttached(); + await expect( + page.locator( + ':nth-match(:text("Control 2"), 2) :text("🦀🦀🦀🦀")' + ) + ).toBeAttached(); + }); + }); + }); + }); + }); +}); From 5314649e5e4dc80ca97c4453430a65ab1b870d74 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:28:15 +0100 Subject: [PATCH 39/44] empty commit to attempt to get around report merger failure From 42fcdd1eaa42be5c104f45e6985040e4abb8d26b Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:30:22 +0000 Subject: [PATCH 40/44] update README --- examples/next/README.md | 1 - examples/next/docs/CHANGELOG.md | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/next/README.md b/examples/next/README.md index 48a7229..bda15c2 100644 --- a/examples/next/README.md +++ b/examples/next/README.md @@ -21,4 +21,3 @@ N.B. NextJs support is currently experimental, see [caveats](#caveats). - The webpack package cannot currently vary some of NextJs' [filesystem convention files](https://nextjs.org/docs/pages/getting-started/project-structure#files-conventions) ([Issue #9](https://github.com/ASOS/web-toggle-point/issues/9)) - The `webpack` plugin uses webpack hooks, so is incompatible with the new TurboPack bundler - The `webpack` plugin uses Node JS APIs to access the filesystem, so may be incompatible with [the edge runtime](https://nextjs.org/docs/app/api-reference/edge#unsupported-apis) -- The `nodeRequestScopedFeaturesStoreFactory` from the [`features`](../../packages/features/docs/README.md) package relies on singleton values held in top-level scope, which Next does not support. See [issue 50](https://github.com/ASOS/web-toggle-point/issues/50), which should remedy this. \ No newline at end of file diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index b6e7bfe..e1366c7 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - update [`Next.js`](https://nextjs.org/) to version 15.5.6 - update [`@playwright/test`](https://github.com/microsoft/playwright/tree/main/packages/playwright-test) to 1.56.0 +### Fixed + +- remove note regarding [issue #50](https://github.com/ASOS/web-toggle-point/issues/50) after release of features [version 0.5.0](../../../packages/features/docs/CHANGELOG.md#050---2025-10-20) + ## [0.4.0] - 2025-10-20 ### Changed From 8e8afbf9c4250ce0a90ba649d5948a8f9c5a105b Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 10 Nov 2025 08:52:18 +0000 Subject: [PATCH 41/44] add missing feature5 to readme --- examples/express/src/routes/parallel-folder-convention/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/express/src/routes/parallel-folder-convention/README.md b/examples/express/src/routes/parallel-folder-convention/README.md index 9e0e08d..a95614c 100644 --- a/examples/express/src/routes/parallel-folder-convention/README.md +++ b/examples/express/src/routes/parallel-folder-convention/README.md @@ -9,6 +9,7 @@ An inbound header named "feature" can take the following values: - feature2 - feature3 - feature4 +- feature5 ...which is used server side (via a ["node request scoped features store"](../../../../../packages/features/docs/README.md#noderequestscopedfeaturesstorefactory)) to generate appropriate server-rendered content. From 55a082fbe81865d214545035ff87f8f2418294e3 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:26:27 +0000 Subject: [PATCH 42/44] no content, to kick off flaky pipeline From 030950cb04de5dea00e0c6e471ca738ae562e033 Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:37:39 +0000 Subject: [PATCH 43/44] empty commit to kick off pipeline From 550e7bda776e4ceb39db63569c69544dc26e134f Mon Sep 17 00:00:00 2001 From: TomStrepsil <10725179+TomStrepsil@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:36:14 +0000 Subject: [PATCH 44/44] fixup download/upload artifacts, to hopefully fix `zipBundleImpl` error when unpacking playwright test report --- .github/workflows/checks.yaml | 8 ++++---- .github/workflows/release.yaml | 12 ++++++------ docs/CHANGELOG.md | 8 +++++++- package.json | 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 | 2 +- packages/webpack/package.json | 2 +- test/automation/docs/CHANGELOG.md | 2 +- 10 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 2171ae1..6acd045 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -67,7 +67,7 @@ jobs: sparse-checkout: | .git/** sparse-checkout-cone-mode: false - + - name: Workaround actions/checkout#1475 run: git config core.sparseCheckout false shell: bash @@ -89,7 +89,7 @@ jobs: working-directory: ./test/automation - name: Upload results for shard - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: ${{ !cancelled() }} with: name: blob-report-${{ matrix.type }}-${{ runner.os }} @@ -117,7 +117,7 @@ jobs: uses: ./.github/actions/setup-node - name: Download blob reports from GitHub Actions Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: path: all-blob-reports pattern: blob-report-* @@ -127,7 +127,7 @@ jobs: run: npx playwright merge-reports --reporter=html ./all-blob-reports - name: Upload HTML report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: html-report--attempt-${{ github.run_attempt }} path: playwright-report diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ef7b596..227c71a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -77,7 +77,7 @@ jobs: - name: Upload updated package.json to artifact if: steps.pre-release-version.outcome == 'success' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ format('{0}-{1}', env.PACKAGE_ARTIFACT, matrix.package.shortName) }} path: | @@ -96,7 +96,7 @@ jobs: - name: Upload to publish matrix if: steps.artifact-json.outcome == 'success' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ format('{0}-{1}', env.UPDATED_MATRIX_ARTIFACT, matrix.package.shortName) }} path: | @@ -111,7 +111,7 @@ jobs: matrix: ${{ steps.updatedPackages.outputs.packageArray }} name: 👨‍🍳 Prepare Publishing Matrix steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: ${{ env.UPDATED_MATRIX_ARTIFACT }}-* merge-multiple: true @@ -151,7 +151,7 @@ jobs: .github/** sparse-checkout-cone-mode: false - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: ${{ env.PACKAGE_ARTIFACT }}-* merge-multiple: true @@ -175,7 +175,7 @@ jobs: shell: bash - name: Upload (potentially reified) package.jsons to artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.PACKAGE_ARTIFACT }} path: ${{ steps.createPathArray.outputs.pathArray }} @@ -204,7 +204,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: ${{ env.PACKAGE_ARTIFACT }} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index af06760..235e5e2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,13 +14,19 @@ N.B. See changelogs for individual packages, where most change will occur: This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo). +## [0.13.1] - 2025-11-14 + +### Changed + +- Moved to v5 of [`upload-artifact`](https://github.com/actions/upload-artifact) and [`download-artifact`](https://github.com/actions/download-artifact) actions +- updated `eslint` to 9.38.0 + ## [0.13.0] - 2025-10-21 ### Changed - updated to `0.11.0` of [`eslint-plugin-workspaces`](https://github.com/joshuajaco/eslint-plugin-workspaces) after [addition of ESLint9 support](https://github.com/joshuajaco/eslint-plugin-workspaces/commit/af855c3a3d8069366d4446747e91828ddf7560c6) - update `eslint.config.mjs` to utilise flat config -- updated `eslint` to 9.38.0 ## [0.12.0] - 2025-09-30 diff --git a/package.json b/package.json index 3459e55..52829d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@asos/web-toggle-point", - "version": "0.13.0", + "version": "0.13.1", "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 720d815..b0efa1d 100644 --- a/packages/features/docs/CHANGELOG.md +++ b/packages/features/docs/CHANGELOG.md @@ -5,7 +5,7 @@ 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.1] - 2025-10-21 +## [0.5.1] - 2025-11-14 ### Fixed diff --git a/packages/react-pointcuts/docs/CHANGELOG.md b/packages/react-pointcuts/docs/CHANGELOG.md index ad11a1c..f79abb4 100644 --- a/packages/react-pointcuts/docs/CHANGELOG.md +++ b/packages/react-pointcuts/docs/CHANGELOG.md @@ -5,7 +5,7 @@ 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.1] - 2025-10-21 +## [0.5.1] - 2025-11-14 ### Fixed diff --git a/packages/ssr/docs/CHANGELOG.md b/packages/ssr/docs/CHANGELOG.md index cf97136..f944019 100644 --- a/packages/ssr/docs/CHANGELOG.md +++ b/packages/ssr/docs/CHANGELOG.md @@ -5,7 +5,7 @@ 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.7] - 2025-10-21 +## [0.2.7] - 2025-11-14 ### Fixed diff --git a/packages/webpack/docs/CHANGELOG.md b/packages/webpack/docs/CHANGELOG.md index 535ebea..4675b45 100644 --- a/packages/webpack/docs/CHANGELOG.md +++ b/packages/webpack/docs/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.9.2] - 2025-10-22 +## [0.9.2] - 2025-11-14 ### Fixed diff --git a/packages/webpack/package.json b/packages/webpack/package.json index b7c4c53..bd33b24 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.9.1", + "version": "0.9.2", "license": "MIT", "type": "module", "main": "./lib/main.cjs", diff --git a/test/automation/docs/CHANGELOG.md b/test/automation/docs/CHANGELOG.md index 2af2dbf..cd2e02b 100644 --- a/test/automation/docs/CHANGELOG.md +++ b/test/automation/docs/CHANGELOG.md @@ -5,7 +5,7 @@ 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.1.5] - 2025-10-21 +## [0.1.5] - 2025-11-14 ### Changed