diff --git a/.gitignore b/.gitignore index 560b8fd..1d4e2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# Test artifacts +coverage/ + # Dependency directories node_modules/ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index af73a0d..d0bea26 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,22 @@ 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-17 + +### Fixed + +- remove `react` and `react-dom` from repo root package.json, introduced in [version 0.9.0](#090---2024-11-29) + +### Added + +- `test:unit:coverage` script + +### Changed + +- set minimum node version to 20.8 due to use of [`import.meta.resolve`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve) where possible in packages / examples +- 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/docs/README.md b/docs/README.md index a59e5f2..23c6280 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,16 +51,18 @@ The bare minimum would be injecting a plugin from the [`webpack package`](../pac Then: -1. Figure out what code modules you'd like to toggle, and see if they are suitable -2. Figure out a filesystem convention to use +1. Figure out what code modules you'd like to toggle. +2. Figure out a filesystem convention to use. - the default proposes colocating variations of base modules in `./__variants__///` folders, with filename parity for the modules themselves. -2. Implement the [`webpack package`](../packages/webpack/docs/README.md)[^2] into your build process, via configuration of appropriate [`pointcuts`](https://en.wikipedia.org/wiki/Pointcut), targeting code modules for toggling that meet the criteria: - - A single, default export, that is a function - - Side-effect free (or, at least, with harmless import side-effects) - - Resolvable by Webpack -3. Create a feature toggle state store, utilising the [`features package`](../packages/features/docs/README.md), or otherwise, suitable for the dynamism of your toggle type. +3. Figure out a ["loading strategy"](../packages/webpack/docs/README.md#loadstrategy) to use. + - the default loads modules at the point they are selected for use, and the code is build into the entry point of the application. + - static (loaded at the entrypoint to the bundle holding the code) or asynchronous (code split) strategies are also available, if compatible with the referring code +4. Implement the [`webpack package`](../packages/webpack/docs/README.md)[^2] into your build process, via configuration of appropriate [`pointcuts`](https://en.wikipedia.org/wiki/Pointcut), targeting code modules for toggling that meet the criteria: + - A single, default export, that is a function. + - Resolvable by Webpack. +5. Create a feature toggle state store, utilising the [`features package`](../packages/features/docs/README.md), or otherwise, suitable for the dynamism of your toggle type. - This needs to get state from an appropriate Toggle Router / runtime state provider. -4. Create a toggle point for the point cuts +6. Create a toggle point for the point cuts. - For a [React](https://react.dev/) application, the `withTogglePointFactory` or `withToggledHookFactory` from the [`react pointcuts package`](../packages/react-pointcuts/docs/README.md) can be used, to construct one. - Again, use [the examples](../examples/README.md) as a guide. 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..508ff3a 100644 --- a/examples/express/docs/CHANGELOG.md +++ b/examples/express/docs/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] - 2025-05-17 + +### Changed + +- added `source-map` devtool and `source-map-loader` to add in visualisation of the module structure in browser developer tools +- fixed to exact version of `react` in `dependencies`, and brought in version-linked `react-is`, a new required peer dependency of the `react-pointcuts` package +- updated the `config` example to utilise the `lazyComponentLoadStrategyFactory` from the `react-pointcuts` package +- updated the `animals` example to utilise the `staticLoadStrategyFactory` from the `webpack` package +- updated `webpack` to `5.99.7` +- update to support new object argument for toggle points introduced by updated webpack plugin +- `MiniCssExtractPlugin` moved to "common" setup block when configuring point cut +- updated to use [`output.module`](https://webpack.js.org/configuration/output/#outputmodule), to help demonstrate this compatibility + - updated [`webpack-node-externals`](https://www.npmjs.com/package/webpack-node-externals) to use `module` [`importType`](https://www.npmjs.com/package/webpack-node-externals#optionsimporttype-commonjs) + +### 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..977e19f 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.3.0", "type": "module", "engines": { "node": ">=20.6.0" @@ -10,9 +10,9 @@ "build-dependencies": "path-exists ../../packages/webpack/lib || npm run --prefix ../../packages/webpack build && path-exists ../../packages/ssr/lib || npm run --prefix ../../packages/ssr build && path-exists ../../packages/features/lib || npm run --prefix ../../packages/features build && path-exists ../../packages/react-pointcuts/lib || npm run --prefix ../../packages/react-pointcuts build", "prebuild": "npm run build-dependencies", "build": "webpack", - "start": "cross-env PORT=3002 node bin/server.cjs", - "start:small-env": "cross-env PORT=3003 node --env-file=./src/routes/config/.env-small bin/server.cjs", - "start:large-env": "cross-env PORT=3004 node --env-file=./src/routes/config/.env-large bin/server.cjs", + "start": "cross-env PORT=3002 node bin/server.mjs", + "start:small-env": "cross-env PORT=3003 node --env-file=./src/routes/config/.env-small bin/server.mjs", + "start:large-env": "cross-env PORT=3004 node --env-file=./src/routes/config/.env-large bin/server.mjs", "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", @@ -20,15 +20,17 @@ "lint:docs": "eslint *.md --flag unstable_config_lookup_from_file" }, "dependencies": { - "@asos/web-toggle-point-react-pointcuts": "file:../../packages/react-pointcuts", "@asos/web-toggle-point-features": "file:../../packages/features", + "@asos/web-toggle-point-react-pointcuts": "file:../../packages/react-pointcuts", "@asos/web-toggle-point-ssr": "file:../../packages/ssr", "@asos/web-toggle-point-webpack": "file:../../packages/webpack", "cross-env": "^7.0.3", "express": "^4.17.1", "http-status-codes": "^2.3.0", - "react": ">=17", - "react-dom": ">=17" + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-is": "^18.3.1", + "source-map-loader": "^5.0.0" }, "devDependencies": { "babel-loader": "^9.2.1", @@ -37,7 +39,7 @@ "path-exists-cli": "^2.0.0", "prop-types": "^15.7.2", "style-loader": "^4.0.0", - "webpack": "^5.38.1", + "webpack": "^5.99.7", "webpack-cli": "^4.7.2", "webpack-node-externals": "^3.0.0" }, diff --git a/examples/express/src/index.js b/examples/express/src/index.js index 11fa87e..66cef45 100644 --- a/examples/express/src/index.js +++ b/examples/express/src/index.js @@ -37,7 +37,7 @@ app.get("/", (_, response) => { diff --git a/examples/express/src/routes/animals/README.md b/examples/express/src/routes/animals/README.md index 3d2386d..d750ca2 100644 --- a/examples/express/src/routes/animals/README.md +++ b/examples/express/src/routes/animals/README.md @@ -19,7 +19,11 @@ It also demonstrates the addition of a toggle-specific side-effect, resulting in You should see a picture of a cat or a dog, depending on the version chosen. The contrived application uses a `streamImage` module that accesses a `urlFetcher`, which is varied by the toggle point. -The toggle point is a higher-order function to ensure that each invocation honours the toggle decision, based on current context. As a caveat of toggling, the constructor of the `urlFetcher` must be called on each request, rather than statically enacted at application start-up. The code demonstrates a mitigation of hypothetical cost of re-construction via use of a cache within the toggle point. +## Implementation + +The variant module loading mode is configured as `static`, meaning all variations are added to the module instance cache when the application starts. + +The toggle point is a higher-order function to ensure that each invocation honours the toggle decision, based on current context, forwarding on arguments to a class constructor. As a caveat of `static` toggling, the constructor of the `urlFetcher` must be called on each request, rather than statically enacted at application start-up. The code demonstrates a mitigation of hypothetical cost of re-construction via use of a cache within the toggle point. The toggle point wraps the varied modules in a [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy) to add the logging side-effect. The use of [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) allows the toggle decision to be scoped on a per-request basis, without explicit access to the express request context. The storage is initialised within [middleware](https://expressjs.com/en/resources/middleware.html). diff --git a/examples/express/src/routes/animals/featuresStore.js b/examples/express/src/routes/animals/featuresStore.js index e446d88..b0e8868 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 { nodeRequestScopedFeaturesStoreFactory as featuresStoreFactory } from "@asos/web-toggle-point-features"; const featuresStore = featuresStoreFactory(); 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/express/src/routes/animals/togglePoint.js b/examples/express/src/routes/animals/togglePoint.js index 5a0d73d..03998c0 100644 --- a/examples/express/src/routes/animals/togglePoint.js +++ b/examples/express/src/routes/animals/togglePoint.js @@ -2,7 +2,7 @@ import featuresStore from "./featuresStore.js"; const cache = new WeakMap(); -export default (_, featuresMap) => { +export default ({ featuresMap }) => { return function (...args) { const { default: Choice } = featuresMap.get( `v${featuresStore.getFeatures().version}` diff --git a/examples/express/src/routes/config/README.md b/examples/express/src/routes/config/README.md index 964acf2..1730d03 100644 --- a/examples/express/src/routes/config/README.md +++ b/examples/express/src/routes/config/README.md @@ -8,6 +8,8 @@ It shows how a payload can be included in the props passed to the toggle point c N.B. No implication that this is in any way a _good use_ of "config", it's heavily contrived. +The implementation uses the `lazyComponentLoadStrategyFactory` from the `react-pointcuts` package, to ensure that variant code is bundled independently, and downloaded on demand. + ## Setup 1. `npm install` @@ -15,5 +17,10 @@ N.B. No implication that this is in any way a _good use_ of "config", it's heavi 3. open `localhost:3002/config` in a browser, you should see a medium sized div 4. stop, and re-start the server with `npm run start:small-env` or `npm run start:large-env` 5. open `localhost:3003/config` (small env) or `localhost:3004/config` (large env), and see a different sized (and coloured) `div` shown -6. press the buttons, to demonstrate overriding the initial content serialized on the server. +6. press the buttons, to demonstrate overriding the initial content serialized on the server - N.B. The colourisation is only a result of the stored config, so using the buttons will just change the size + - watch the network tab in developer tools to observe the lazy loading in effect + - try blocking one of the subsequent chunks, you should see the error boundary falling back to the default experience, with a console log: + ``` + ChunkLoadError: Variant errored, rendering fallback: Loading chunk ### failed. + ``` diff --git a/examples/express/src/routes/config/router.js b/examples/express/src/routes/config/router.js index d12d087..bc5e107 100644 --- a/examples/express/src/routes/config/router.js +++ b/examples/express/src/routes/config/router.js @@ -31,7 +31,7 @@ router.get("/*", (req, res) => { , { - bootstrapScripts: ["/config/main.js"], + bootstrapModules: ["/config/main.mjs"], onShellReady() { res.statusCode = 200; res.setHeader("Content-type", "text/html"); diff --git a/examples/express/webpack.config.js b/examples/express/webpack.config.js index 73d72f4..2816d04 100644 --- a/examples/express/webpack.config.js +++ b/examples/express/webpack.config.js @@ -1,55 +1,66 @@ import { resolve, basename, dirname, posix } from "path"; +import { fileURLToPath } from "url"; import externals from "webpack-node-externals"; -import { TogglePointInjection } from "@asos/web-toggle-point-webpack/plugins"; +import { TogglePointInjectionPlugin } from "@asos/web-toggle-point-webpack/plugins"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; -import { fileURLToPath } from "url"; +import staticLoadStrategyFactory from "@asos/web-toggle-point-webpack/moduleLoadStrategyFactories/staticLoadStrategyFactory"; +import lazyComponentLoadStrategyFactory from "@asos/web-toggle-point-react-pointcuts/lazyComponentLoadStrategyFactory"; const configPointCutConfig = { name: "configuration variants", variantGlobs: ["./src/routes/config/__variants__/*/*/*.jsx"], - togglePointModule: "/src/routes/config/togglePoint.js" + togglePointModuleSpecifier: "/src/routes/config/togglePoint.js", + loadStrategy: lazyComponentLoadStrategyFactory(), }; const common = { mode: "production", + devtool: "source-map", + experiments: { + outputModule: true, + }, + output: { + module: true, + }, module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: { - loader: "babel-loader" - } + loader: "babel-loader", + }, }, { test: /\.css$/, - use: [MiniCssExtractPlugin.loader, "css-loader"] - } - ] - } + use: [MiniCssExtractPlugin.loader, "css-loader"], + }, + ], + }, + plugins: [new MiniCssExtractPlugin()], }; const config = [ { entry: "./src/index.js", target: "node", + ...common, output: { + ...common.output, path: resolve(dirname(fileURLToPath(import.meta.url)), "bin"), - filename: "server.cjs", + filename: "server.mjs", clean: true, - chunkFormat: "module" }, - externals: [externals()], - ...common, + externals: [externals({ importType: "module" })], plugins: [ - new MiniCssExtractPlugin(), - new TogglePointInjection({ + ...common.plugins, + new TogglePointInjectionPlugin({ pointCuts: [ configPointCutConfig, { name: "animal apis by version", variantGlobs: [ - "./src/routes/animals/api/**/v{1..9}*([[:digit:]])/*.js" + "./src/routes/animals/api/**/v{1..9}*([[:digit:]])/*.js", ], joinPointResolver: (variantPath) => posix.resolve( @@ -57,25 +68,38 @@ const config = [ ...Array(3).fill(".."), basename(variantPath) ), - togglePointModule: "/src/routes/animals/togglePoint.js" - } - ] - }) - ] + togglePointModuleSpecifier: "/src/routes/animals/togglePoint.js", + loadStrategy: staticLoadStrategyFactory(), + }, + ], + }), + ], }, { entry: "./src/routes/config/client.js", target: "web", + ...common, output: { + ...common.output, path: resolve(dirname(fileURLToPath(import.meta.url)), "public"), - filename: "main.js" + filename: "main.mjs", }, plugins: [ - new MiniCssExtractPlugin(), - new TogglePointInjection({ pointCuts: [configPointCutConfig] }) + ...common.plugins, + new TogglePointInjectionPlugin({ pointCuts: [configPointCutConfig] }), ], - ...common - } + module: { + ...common.module, + rules: [ + { + test: /\.js$/, + enforce: "pre", + use: ["source-map-loader"], + }, + ...common.module.rules, + ], + }, + }, ]; export default config; diff --git a/examples/next/README.md b/examples/next/README.md index 9ff4860..0419071 100644 --- a/examples/next/README.md +++ b/examples/next/README.md @@ -1,48 +1,25 @@ # 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 `webpack` plugin's default loading strategy (`deferredRequire`) is incompatible with the pages router in Next 14 (and presumed below), due to [issues](https://github.com/vercel/next.js/discussions/37520) whereby next converts deferred require into a `Promise`. It's fixed in Next 15 +- The `nodeRequestScopedFeaturesStoreFactory` relies on singleton values held in top-level scope, which Next does not support due to [issues](https://github.com/vercel/next.js/issues/65350#issuecomment-2318650615). This may be mitigated via use of a cross-realm Symbol (see [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#shared_symbols_in_the_global_symbol_registry)) in a future update, as works with [`express-http-context`](https://github.com/skonves/express-http-context) \ No newline at end of file diff --git a/examples/next/docs/CHANGELOG.md b/examples/next/docs/CHANGELOG.md index 91c3ed8..30d2f72 100644 --- a/examples/next/docs/CHANGELOG.md +++ b/examples/next/docs/CHANGELOG.md @@ -5,6 +5,25 @@ 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-07-17 + +### Added + +- added a "content management" example, demonstrating use of `withToggledHookFactory` from the `react-pointcuts` package + +### Changed + +- updated to new webpack plugin + - moved "experiments" example to use the `lazyComponentLoadStrategyFactory` from `react-pointcuts` + - new "content management" example utilising the default `deferredRequireLoadStrategyFactory` of `webpack` package +- colocate documentation for "experiments" example to sit with its own `README.mdx` +- updated documentation to indicate incompatibility of Next 14 (and presumed below) with the default `deferredRequireLoadStrategyFactory` + +### Fixed + +- consistent "Explanation" and "Activation" sections in example `README.mdx` files +- removed errant `toggle-point.d.ts` in `tsconfig.json` + ## [0.2.4] - 2025-05-27 ### Changed @@ -22,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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.config.mjs b/examples/next/next.config.mjs index 428a031..643b651 100644 --- a/examples/next/next.config.mjs +++ b/examples/next/next.config.mjs @@ -1,37 +1,40 @@ import createMDX from "@next/mdx"; import remarkGfm from "remark-gfm"; +import { TogglePointInjectionPlugin } 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"; /** @type {import('next').NextConfig} */ const nextConfig = { - pageExtensions: ["js", "md", "mdx", "ts", "tsx"] + 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" - ] - } - ], - webpackNormalModule: async () => - (await import("next/dist/compiled/webpack/NormalModule.js")).default +const webpackNormalModule = async () => + (await import("next/dist/compiled/webpack/NormalModule.js")).default; + +const togglePointInjection = new TogglePointInjectionPlugin({ + pointCuts: [...experimentPointCutConfig, ...contentManagementPointCutConfig], + webpackNormalModule, }); nextConfig.webpack = (config) => { return { ...config, - plugins: [...config.plugins, togglePointInjection] + plugins: [...config.plugins, togglePointInjection], + resolve: { + ...(config.resolve ?? {}), + alias: { + ...(config.resolve.alias ?? {}), + "react-is": "next/dist/compiled/react-is/cjs/react-is.production.js", + }, + }, }; }; const withMDX = createMDX({ options: { - remarkPlugins: [remarkGfm] - } + remarkPlugins: [remarkGfm], + }, }); export default withMDX(nextConfig); diff --git a/examples/next/package.json b/examples/next/package.json index e1d85f0..302ed7d 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.3.0", "private": true, "type": "module", "scripts": { @@ -17,20 +17,20 @@ "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.3.0", + "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", "eslint-config-next": "^15.0.3", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-mdx": "^3.1.5", 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..9aa47c8 --- /dev/null +++ b/examples/next/src/app/fixtures/content-management/__pointCutConfig.js @@ -0,0 +1,10 @@ +export default [ + { + name: "content management", + togglePointModuleSpecifier: + "/src/app/fixtures/content-management/withToggledHook", + variantGlobs: [ + "./src/app/fixtures/content-management/**/__variants__/*/*/use!(*.spec).ts" + ] + } +]; diff --git a/examples/next/src/app/fixtures/content-management/__variants__/devMode/active/useContentEditable.ts b/examples/next/src/app/fixtures/content-management/__variants__/devMode/active/useContentEditable.ts new file mode 100644 index 0000000..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(/