Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ N.B. See changelogs for individual packages, where most change will occur:

This log covers the [monorepo](https://en.wikipedia.org/wiki/Monorepo).

## [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
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
A library providing a means to toggle or branch web application code.
</p>
<p align="center">
<a href="https://github.com/ASOS/web-toggle-point/tags/"><img src="https://img.shields.io/github/tag/ASOS/web-toggle-point" alt="Current version" /></a>
<img src="https://github.com/ASOS/web-toggle-point/workflows/Pull Request Checks/badge.svg" alt="Current test status" />
<a href="http://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs are welcome" /></a>
<a href="https://github.com/ASOS/web-toggle-point/issues/"><img src="https://img.shields.io/github/issues/ASOS/web-toggle-point" alt="toggle point issues" /></a>
<a href="https://github.com/ASOS/web-toggle-point/discussions/"><img src="https://img.shields.io/github/discussions/ASOS/web-toggle-point" alt="toggle point discussions" /></a>
<img src="https://img.shields.io/github/stars/ASOS/web-toggle-point" alt="toggle point stars" />
<img src="https://img.shields.io/github/forks/ASOS/web-toggle-point" alt="toggle point forks" />
<img src="https://img.shields.io/github/license/ASOS/web-toggle-point" alt="toggle point license" />
Expand Down
46 changes: 11 additions & 35 deletions examples/next/README.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 21 additions & 2 deletions examples/next/docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-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
Expand All @@ -19,19 +37,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

Expand Down
1 change: 1 addition & 0 deletions examples/next/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
18 changes: 6 additions & 12 deletions examples/next/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
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} */
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"
]
}
],
webpackNormalModule: async () =>
(await import("next/dist/compiled/webpack/NormalModule.js")).default
pointCuts: [...experimentPointCutConfig, ...contentManagementPointCutConfig],
webpackNormalModule
});

nextConfig.webpack = (config) => {
Expand Down
15 changes: 8 additions & 7 deletions examples/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web-toggle-point-next-example",
"version": "0.2.5",
"version": "0.3.0",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -17,20 +17,21 @@
"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",
"@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",
Expand Down
18 changes: 18 additions & 0 deletions examples/next/src/app/fixtures/content-management/README.mdx
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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"
]
}
];
Original file line number Diff line number Diff line change
@@ -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<TurndownService>(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(/<script>(.*?)<\/script>/g, "")!
)!
);
});
}
};

useEffect(() => {
void (async () => {
if (!turndownServiceRef.current) {
const { default: Service } = (await import("turndown")) as {
default: typeof TurndownService;
};
turndownServiceRef.current = new Service();
}
})();
document.designMode = "on";
document.body.setAttribute("data-design-mode", "true");
document.addEventListener("keydown", okKeyDown, false);

return () => {
document.designMode = "off";
document.body.removeAttribute("data-design-mode");
document.removeEventListener("keydown", okKeyDown, false);
};
}, []);
};

export default useContentEditable;
17 changes: 17 additions & 0 deletions examples/next/src/app/fixtures/content-management/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use server";
import { writeFile, readFile } from "fs/promises";
import { join, dirname } from "path";
import { fileURLToPath } from "url";

const markdownPath = join(
dirname(fileURLToPath(import.meta.url)),
"README.mdx"
);

export async function readMarkdown() {
return (await readFile(markdownPath)).toString();
}

export async function saveMarkdown(stuff: string) {
await writeFile(markdownPath, stuff);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";
import type { ReactNode } from "react";
import featuresStore from "./featuresStore";

const FeaturesProvider = featuresStore.providerFactory();

interface FeaturesProviderProps {
children: ReactNode;
isContentEditor: boolean;
}

export default ({ children, isContentEditor }: FeaturesProviderProps) => {
return (
<FeaturesProvider
value={{ devMode: { status: isContentEditor ? "active" : "inactive" } }}
>
{children}
</FeaturesProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import featuresStoreFactory from "@asos/web-toggle-point-features/storeFactories/reactContextFeaturesStoreFactory";

const reactContextStore = featuresStoreFactory({
name: "Content Management"
});

export default reactContextStore;
19 changes: 19 additions & 0 deletions examples/next/src/app/fixtures/content-management/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import ContentEditorContext from "./contentEditorContext";
import type { ReactNode } from "react";
import { saveMarkdown, readMarkdown } from "./actions";
import { cookies } from "next/headers";
import "./styles.css";

interface LayoutProps {
children: ReactNode;
}

export default async function Layout({ children }: LayoutProps) {
const isContentEditor = (await cookies()).has("i-am-a-content-editor");
saveMarkdown(await readMarkdown()); // Server Actions appear to need to be called in the server component first before they can be used in the client component... Hopefully Next will fix.
return (
<ContentEditorContext {...{ isContentEditor }}>
{children}
</ContentEditorContext>
);
}
12 changes: 12 additions & 0 deletions examples/next/src/app/fixtures/content-management/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import type { JSX } from "react";
import ReadMe from "./README.mdx";
import useContentEditable from "./useContentEditable";

const Page = (): JSX.Element => {
useContentEditable();
return <ReadMe />;
};

export default Page;
Loading
Loading