Skip to content

Conversation

@TomStrepsil
Copy link
Contributor

@TomStrepsil TomStrepsil commented May 18, 2025

Issue

Allow for different module loading strategies, adding the ability to code-split variation code.

resolves #16

Details

webpack package updates

Update the webpack plugin to ingest a strategy for generating import code, such that each point cut may define how code is accessed at run-time.

At present, all code is statically required at application bootstrap, via the use of import.meta.webpackContext. Unadorned, this method causes all variation modules in a potential join points to be statically imported, thus all side-effects of all modules are run. This requires toggled code to be side-effect free, or have side-effects that do not interfere with each other / can act idempotently, when identical. It also means all the varied code is included in the bundle that contains the base/varied files, potentially bloating the base experience.

Although the import.meta.webpackContext method supports adornment with a "mode" parameter that can take lazy and eager as a means of deferring import, these necessarily output a Promise, thus can only be compatible with consuming code that is asynchronously importing the module to be varied.

To support other schemes, such as a deferred require, the plugin has been updated to accept an optional loadStrategy configuration parameter. These strategies manually provide that which the webpack context module api afforded, preparing all potential codepaths based on the variations found by the point cut configuration.

load strategy factories

Three factories for such strategies are provided, as package exports:

  • staticLoadStrategyFactory

This produces code equivalent to the import.meta.webpackContext / context module api scheme (so parity with the current version), whereby all variant modules are statically imported at the point the chunk entrypoint is imported. At this time, all side-effects will execute in series.

This is no longer the default option, since the common case assumes the overhead and impact of side-effects for an irrelevant module selection should be avoided. However, if parity with the existing scheme is deemed desirable, perhaps to avoid costly just-in-time code execution, this option is still available.

  • deferredRequireLoadStrategyFactory

This produces modules wrapped in a synchronous function, that will require the module at the point that it's selected.
Despite using require (a commonJs feature), it appears compatible with output.module, with the serve example updated to use this output format.

This is the default load strategy, if one is not specified.

N.B. Due to oddness in NextJs 14 see here this produces Promise out of the Webpack build, so appears incompatible. Next 15 (app or pages router) does not appear to suffer the same.

  • deferredDynamicImportLoadStrategyFactory

This produces modules wrapped in an asynchronous function that dynamically imports the module at the point that it's selected. This hence returns a Promise, and the toggle point needs to be compatible with asynchronous access.

Webpack, unless overridden, will code-split the point cuts, creating non-initial chunks. This should ensure that the "base" version of the application has no increased initial chunk size.

Strategy factory modules

Since a strategy needs to represent both code that is executed at compile time, and provide code that is baked into the compilation to be executed at run-time, the interface of these modules is somewhat strange.

The default export represents the factory for a importCodeGenerator, used to generate the import code itself at compile-time.

It can also have named exports that are baked into the compilation to "pack" and "unpack" the modules that the importCodeGenerator has accessed. This provides the mean to defer import and/or execution, or otherwise mutate the module at the point of storage in "feature store", and mutate back into an executable form, when it's deemed the module is relevant for the toggle state.

react-pointcuts package updates

The package provides it's own load strategy factory, that composes the deferredDynamicImportLoadStrategyFactory, for its importCodeGenerator method, but setting the "pack" option to wrap the dynamic import statement (comprising a load function) in React.lazy.

The withToggleHandlerFactory is updated to detect lazy-wrapped modules, wrapping in Suspense if detected. The suspense boundary is backed by a null fallback, to allow server-side rendering to maintain the existing markup as a lazy bundle is downloaded prior to hydration.

Where available (React 18+) it also wraps the component in useDeferredValue such that reactive feature stores can change the selected variation whilst maintaining the mount of the prior variation as new chunks are downloaded. This avoids any "flash of no content" as the variant selection is changing.

Updated the withToggledHookFactory to support "unpacking" of modules.

next example updates

Added a new "content management" example as a fixture for the previously un-tested withToggledHookFactory
Added note regarding NextJS-compiled react-is alias'ing

Scout Rule

weback package

  • renamed the TogglePointInjection plugin to have a Plugin suffix, supporting a standard webpack naming convention
  • updated the toggleHandler package exports to be "factories", now available under a toggleHandlerFactories/ exports path
  • remove next peer dependency, there's no reason to be explicit
  • ensured unresolvable files (can happen if node_modules part of the app root, and something odd has happened) are skipped

react-pointcuts package

  • clarified the features store structure that the toggle points are compatible with

test/automation

  • added ui scripts for playwright ui mode

features package

  • added missing test coverage for ssrBackedReactContext store

serve example

  • moved to production webpack mode with source-map devtool and source-map-loader, for clarity when using dev tools

next example

  • consistent "Explanation" and "Activation" sections in example README.mdx files
  • remove errant toggle-point.d.ts from tsconfig.json

express example

  • added source-map devtool and source-map-loader, for clarity when using dev tools
  • removed "Vary" header from "animals" example, the page is meant to be un-cacheable, and the value was wrong in any case
  • use output.module, to help demonstrate this compatibility

repo root

Upgrade guide

This change will represent breaking changes in consumers of the webpack package.

  • the TogglePointInjection named export is now named TogglePointInjectionPlugin

The point cut configuration needs to be modified for the plugin thus:

  • togglePointModule option is renamed to togglePointModuleSpecifier
  • toggleHandler option is renamed to toggleHandlerFactoryModuleSpecifier
    • if this is configured, it will need to instead point to a factory method for the toggle handler, rather than the handler itself.

N.B. The existing code has a "load strategy" based on import.meta.webpackContext.

For complete parity with this, a strategy built by the staticLoadStrategyFactory should be specified (as the loadStrategy), but would suggest trying the new default which should be functionally identical, and avoid needless side-effects. However, the pages router in NextJs 14 and below appears to convert require() into import() (see issue, and above), so the default should be avoided in this setup.

CheckList

  • PR starts with [ISSUE_ID].
  • Has been tested (where required) before merge to main.

TomStrepsil and others added 30 commits December 24, 2024 17:31
* 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
* 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 <[email protected]>
@asos-dominicjomaa
Copy link

Phoar, this is a big one 🚨

@TomStrepsil TomStrepsil requested a review from a team as a code owner July 22, 2025 16:48
@TomStrepsil TomStrepsil marked this pull request as draft July 29, 2025 21:36
@TomStrepsil
Copy link
Contributor Author

Draft - waiting for merge down after #55 is merged, since it carves significant chunks from this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Lazy Code Splits

2 participants