From 357d87afe6588ae030b48154fb02b9fcbac2346c Mon Sep 17 00:00:00 2001 From: Kelly Mears Date: Wed, 20 Dec 2023 14:12:26 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A9=B9=20fix(patch):=20WordPress=20module?= =?UTF-8?q?=20reload=20failures=20(#2530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes #2529 and other associated problems with hot module reload in certain WordPress environments. ## The problem If `SCRIPT_DEBUG` is not set and a script declares `wp-react-refresh-runtime` as a dependency, WordPress will _**silently** omit the entire enqueue request_. What the... 🙈 Bedrock installs don't trigger this behavior because Bedrock defines it: https://github.com/roots/bedrock/blob/master/config/environments/development.php#L14 ## Solution This change omits including `wp-react-refresh-runtime` in `entrypoints.json` and does not externalize `react-refresh/runtime.js` if `SCRIPT_DEBUG` isn't set in a .env file somewhere. ## Type of change **PATCH: backwards compatible change** --- .../bud-preset-wordpress/src/extension.ts | 194 +++++++++--------- .../@roots/bud-preset-wordpress/src/index.ts | 6 +- .../@roots/bud-react/src/extension/index.ts | 4 + .../bud-wordpress-theme-json/src/extension.ts | 191 ++++++++++------- .../bud-wordpress-theme-json/src/index.ts | 6 +- .../src/plugin.ts | 21 ++ .../src/plugin.ts | 21 ++ .../src/index.ts | 2 +- .../src/plugin.ts | 4 + tests/reproductions/issue-2126/theme.json | 4 +- 10 files changed, 270 insertions(+), 183 deletions(-) diff --git a/sources/@roots/bud-preset-wordpress/src/extension.ts b/sources/@roots/bud-preset-wordpress/src/extension.ts index 016a1c1681..33b7ed436e 100644 --- a/sources/@roots/bud-preset-wordpress/src/extension.ts +++ b/sources/@roots/bud-preset-wordpress/src/extension.ts @@ -1,12 +1,15 @@ import type {Bud} from '@roots/bud-framework' import type BudWordPressDependencies from '@roots/bud-wordpress-dependencies' import type BudWordPressExternals from '@roots/bud-wordpress-externals' -import type WordPressThemeJSON from '@roots/bud-wordpress-theme-json' +import type WordPressThemeJson from '@roots/bud-wordpress-theme-json' + +import {join} from 'node:path' import { + DynamicOption, Extension, - type Option, - type PublicExtensionApi, + type OptionGetter, + type OptionSetter, } from '@roots/bud-framework/extension' import { bind, @@ -25,80 +28,69 @@ interface Options { exclude: Array hmr: boolean notify: boolean + scriptDebug: boolean } /** - * WordPress Preset API + * WordPress preset */ -interface PublicExtension extends PublicExtensionApi { - /** - * {@link BudWordPressDependencies} - */ - dependencies: BudWordPressDependencies +@label(`@roots/bud-preset-wordpress`) +@dependsOn([ + `@roots/bud-preset-recommend`, + `@roots/bud-wordpress-externals`, + `@roots/bud-wordpress-dependencies`, + `@roots/bud-wordpress-theme-json`, + `@roots/bud-react`, +]) +@options({ + exclude: [], + hmr: true, + notify: true, + scriptDebug: DynamicOption.make(({env}) => env.isTrue(`SCRIPT_DEBUG`)), +}) +@expose(`wp`) +export default class BudPresetWordPress extends Extension { + public declare scriptDebug: Options[`scriptDebug`] + public declare getScriptDebug: OptionGetter + public declare setScriptDebug: OptionSetter< + BudPresetWordPress, + Options, + `scriptDebug` + > + /** * Exclude dependencies from externals and the `entrypoints.json` manifest * * @default [] */ - exclude: Option[`value`] - /** - * {@link BudWordPressExternals} - */ - externals: BudWordPressExternals - + public declare readonly exclude: Options[`exclude`] /** * Get excluded dependencies * * @returns Array */ - getExclude: Option[`get`] + public declare getExclude: OptionGetter /** - * Get `@roots/wordpress-hmr` functionality - * - * @returns boolean - */ - getHmr: Option[`get`] - /** - * Get WordPress editor toast notifications - * - * @returns boolean + * Set excluded dependencies */ - getNotify: Option[`get`] + public declare setExclude: OptionSetter< + BudPresetWordPress, + Options, + `exclude` + > /** * Enable `@roots/wordpress-hmr` functionality * * @default true */ - hmr: Option[`value`] + public declare readonly hmr: Options[`hmr`] /** - * {@link WordPressThemeJSON} - */ - json: WordPressThemeJSON - /** - * WordPress editor toast notifications - * - * @default true - */ - notify: Option[`value`] - - /** - * Set excluded dependencies - * - * @param value Array - * @returns this - * - * @example - * ```js - * bud.wp.setExclude(['react']) - * ``` + * Get `@roots/wordpress-hmr` functionality * - * @example - * ```js - * bud.wp.setExclude(exclude => [...exclude, 'react']) - * ``` + * @returns boolean */ - setExclude: Option[`set`] + public declare getHmr: OptionGetter /** * Set `@roots/wordpress-hmr` functionality * @@ -115,9 +107,22 @@ interface PublicExtension extends PublicExtensionApi { * bud.wp.setHmr(hmr => false) * ``` */ - setHmr: Option[`set`] + public declare setHmr: OptionSetter + + /** + * WordPress editor toast notifications value + * + * @returns boolean + */ + public declare notify: Options[`notify`] + /** + * Get WordPress editor toast notifications + * + * @returns boolean + */ + public declare getNotify: OptionGetter /** - * Set WordPress editor toast notifications + * Toggle WordPress editor toast notifications * * @param value boolean * @returns this @@ -132,50 +137,31 @@ interface PublicExtension extends PublicExtensionApi { * bud.wp.setNotify(notify => false) * ``` */ - setNotify: Option[`set`] -} - -/** - * WordPress preset - */ -@label(`@roots/bud-preset-wordpress`) -@dependsOn([ - `@roots/bud-preset-recommend`, - `@roots/bud-wordpress-externals`, - `@roots/bud-wordpress-dependencies`, - `@roots/bud-wordpress-theme-json`, - `@roots/bud-react`, -]) -@options({ - exclude: [], - hmr: true, - notify: true, -}) -@expose(`wp`) -export default class BudPresetWordPress - extends Extension - implements PublicExtension -{ - public declare exclude: PublicExtension[`exclude`] - public declare getExclude: PublicExtension[`getExclude`] - public declare setExclude: PublicExtension[`setExclude`] - - public declare hmr: PublicExtension[`hmr`] - public declare getHmr: PublicExtension[`getHmr`] - public declare setHmr: PublicExtension[`setHmr`] - - public declare notify: PublicExtension[`notify`] - public declare getNotify: PublicExtension[`getNotify`] - public declare setNotify: PublicExtension[`setNotify`] + public declare setNotify: OptionSetter< + BudPresetWordPress, + Options, + `notify` + > + /** + * {@link BudWordPressDependencies} + */ public get dependencies(): BudWordPressDependencies { return this.app.extensions.get(`@roots/bud-wordpress-dependencies`) } + /** + * {@link BudWordPressExternals} + */ public get externals(): BudWordPressExternals { return this.app.extensions.get(`@roots/bud-wordpress-externals`) } - public get json(): WordPressThemeJSON { - return this.app.extensions.get(`@roots/bud-wordpress-theme-json`) + /** + * {@link WordPressThemeJson} + */ + public get json(): WordPressThemeJson { + return this.app.extensions.get( + `@roots/bud-wordpress-theme-json`, + ) as unknown as WordPressThemeJson } /** @@ -186,7 +172,12 @@ export default class BudPresetWordPress await this.compilerCheck(bud) if (bud.extensions.has(`@roots/bud-tailwindcss`)) - await bud.extensions.add(`@roots/bud-tailwindcss-theme-json`) + await bud.extensions.add( + await this.resolve( + `@roots/bud-tailwindcss-theme-json`, + import.meta.url, + ), + ) } /** @@ -211,6 +202,19 @@ export default class BudPresetWordPress provided.set(`React`, undefined) } + /** + * If `SCRIPT_DEBUG` env value is not set, exclude `react-refresh/runtime` from externals + * and inclusion in entrypoints.json dependencies array(s). + * + * Unless user has manually overridden this. Common example: if they have set SCRIPT_DEBUG + * directly in their WordPress config file (which bud.js does not have access to it). + */ + !this.getScriptDebug() && + this.setExclude((exclude = []) => [ + ...exclude, + join(`react-refresh`, `runtime`), + ]) + /** * Exclude anything specified in {@link Options.exclude} */ @@ -224,7 +228,7 @@ export default class BudPresetWordPress @bind private async handleHmr({build, hooks}: Bud) { /** Bail if hmr option is false */ - if (!this.hmr) return + if (!this.getHmr()) return /** Source loader */ const loader = await this.resolve( @@ -249,7 +253,7 @@ export default class BudPresetWordPress .setItem(`@roots/wordpress-hmr/loader`, { loader: `@roots/wordpress-hmr/loader`, options: { - notify: this.get(`notify`), + notify: this.getNotify(), }, }) @@ -340,5 +344,3 @@ export default class BudPresetWordPress } } } - -export type {PublicExtension} diff --git a/sources/@roots/bud-preset-wordpress/src/index.ts b/sources/@roots/bud-preset-wordpress/src/index.ts index ff66f2cc7c..c683e2c7a5 100644 --- a/sources/@roots/bud-preset-wordpress/src/index.ts +++ b/sources/@roots/bud-preset-wordpress/src/index.ts @@ -8,13 +8,11 @@ * @see https://github.com/roots/bud */ -import BudPresetWordPress, { - type PublicExtension, -} from '@roots/bud-preset-wordpress/extension' +import BudPresetWordPress from '@roots/bud-preset-wordpress/extension' declare module '@roots/bud-framework' { interface Bud { - wp: PublicExtension + wp: BudPresetWordPress } interface Modules { diff --git a/sources/@roots/bud-react/src/extension/index.ts b/sources/@roots/bud-react/src/extension/index.ts index 8e89b327e7..f33a47efa2 100644 --- a/sources/@roots/bud-react/src/extension/index.ts +++ b/sources/@roots/bud-react/src/extension/index.ts @@ -38,6 +38,10 @@ export default class BudReact extends Extension { bud.swc.setJsc( merge(bud.swc.jsc, {transform: {react: {runtime: `automatic`}}}), ) + bud.swc.setTransform((transform = {}) => ({ + react: {runtime: `automatic`, ...(transform.react ?? {})}, + ...transform, + })) } if (bud.babel) { diff --git a/sources/@roots/bud-wordpress-theme-json/src/extension.ts b/sources/@roots/bud-wordpress-theme-json/src/extension.ts index f372063b46..3f25798646 100644 --- a/sources/@roots/bud-wordpress-theme-json/src/extension.ts +++ b/sources/@roots/bud-wordpress-theme-json/src/extension.ts @@ -3,7 +3,8 @@ import type {Options} from '@roots/wordpress-theme-json-webpack-plugin' import { DynamicOption, Extension, - type StrictPublicExtensionApi, + type OptionGetter, + type OptionSetter, } from '@roots/bud-framework/extension' import { bind, @@ -28,55 +29,6 @@ interface Mutator { (json: Partial): Partial } -type Api = StrictPublicExtensionApi< - WordPressThemeJSON, - Options & Record -> & { - customTemplates: Options['customTemplates'] - generated: Options['__generated__'] - path: Options['path'] - patterns: Options['patterns'] - /** - * ## bud.wpjson.settings - * - * Define `theme.json` settings using an options object or callback - */ - settings: WordPressThemeJSON[`settings`] - styles: Options['styles'] - templateParts: Options['templateParts'] - /** - * ## bud.wpjson.useTailwindColors - * - * Source `theme.json` color values from `tailwind.config.js` - * - * @note - * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. - */ - useTailwindColors: (value?: boolean, extendOnly?: boolean) => Api - - /** - * ## bud.wpjson.useTailwindFontFamily - * - * Source `theme.json` fontFamily values from `tailwind.config.js` - * - * @note - * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. - */ - useTailwindFontFamily: (value?: boolean, extendOnly?: boolean) => Api - - /** - * ## bud.wpjson.useTailwindFontSize - * - * Source `theme.json` fontSize values from `tailwind.config.js` - * - * @note - * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. - */ - useTailwindFontSize: (value?: boolean, extendOnly?: boolean) => Api - - version: Options['version'] -} - /** * WordPress theme.json configuration * @@ -89,7 +41,7 @@ type Api = StrictPublicExtensionApi< */ @label(`@roots/bud-wordpress-theme-json`) @options({ - __generated__: undefined, + __generated__: `⚠️ This file is generated. Do not edit.`, customTemplates: undefined, path: DynamicOption.make(({path}) => path(`./theme.json`)), patterns: undefined, @@ -113,38 +65,123 @@ type Api = StrictPublicExtensionApi< }, styles: undefined, templateParts: undefined, + title: undefined, version: undefined, }) @plugin(ThemeJsonWebpackPlugin) @expose(`wpjson`) @disabled -class WordPressThemeJSON - extends Extension - implements Api -{ - public declare readonly customTemplates: Api['customTemplates'] - public declare getCustomTemplates: Api['getCustomTemplates'] - public declare getPath: Api['getPath'] - public declare getPatterns: Api['getPatterns'] - public declare getSettings: Api['getSettings'] - public declare getStyles: Api['getStyles'] - public declare getTemplateParts: Api['getTemplateParts'] - public declare getVersion: Api['getVersion'] - public declare readonly path: Api['path'] - public declare readonly patterns: Api['patterns'] - public declare setCustomTemplates: Api['setCustomTemplates'] - public declare setPath: Api['setPath'] - public declare setPatterns: Api['setPatterns'] - public declare setSettings: Api['setSettings'] - public declare setStyles: Api['setStyles'] - public declare setTemplateParts: Api['setTemplateParts'] - public declare setVersion: Api['setVersion'] - public declare readonly styles: Api['styles'] - public declare readonly templateParts: Api['templateParts'] - public declare readonly version: Api['version'] +class WordPressThemeJson extends Extension< + Options, + ThemeJsonWebpackPlugin +> { + public declare customTemplates: Options['customTemplates'] + public declare getCustomTemplates: OptionGetter< + Options, + `customTemplates` + > + public declare setCustomTemplates: OptionSetter< + WordPressThemeJson, + Options, + `customTemplates` + > + + public declare readonly __generated__: Options[`__generated__`] + public declare get__generated__: OptionGetter + public declare set__generated__: OptionSetter< + WordPressThemeJson, + Options, + `__generated__` + > + + public declare readonly path: Options['path'] + public declare getPath: OptionGetter + public declare setPath: OptionSetter + + public declare readonly patterns: Options['patterns'] + public declare getPatterns: OptionGetter + public declare setPatterns: OptionSetter< + WordPressThemeJson, + Options, + `patterns` + > + + public declare getSettings: OptionGetter + public declare setSettings: OptionSetter< + WordPressThemeJson, + Options, + `settings` + > + + public declare readonly styles: Options['styles'] + public declare getStyles: OptionGetter + public declare setStyles: OptionSetter< + WordPressThemeJson, + Options, + `styles` + > + + public declare readonly title: Options['title'] + public declare getTitle: OptionGetter + public declare setTitle: OptionSetter< + WordPressThemeJson, + Options, + `title` + > + + public declare readonly version: Options['version'] + public declare getVersion: OptionGetter + public declare setVersion: OptionSetter< + WordPressThemeJson, + Options, + `version` + > + + public declare readonly templateParts: Options['templateParts'] + public declare getTemplateParts: OptionGetter + public declare setTemplateParts: OptionSetter< + WordPressThemeJson, + Options, + `templateParts` + > + + /** + * ## bud.wp.json.useTailwindColors + * + * Source `theme.json` fontSize values from `tailwind.config.js` + * + * @note + * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. + */ + public declare useTailwindColors?: ( + value?: boolean, + ) => WordPressThemeJson + + /** + * ## bud.wp.json.useTailwindFontFamily + * + * Source `theme.json` fontFamily values from `tailwind.config.js` + * + * @note + * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. + */ + public declare useTailwindFontFamily?: ( + value?: boolean, + ) => WordPressThemeJson + + /** + * ## bud.wp.json.useTailwindFontSize + * + * Source `theme.json` fontSize values from `tailwind.config.js` + * + * @note + * Requires {@link https://bud.js.org/extensions/bud-tailwindcss/ @roots/bud-tailwindcss} to be installed. + */ + public declare useTailwindFontSize?: ( + value?: boolean, + ) => WordPressThemeJson @bind - // @ts-ignore public settings( input: | boolean @@ -184,4 +221,4 @@ class WordPressThemeJSON } } -export {type Api, WordPressThemeJSON} +export {WordPressThemeJson} diff --git a/sources/@roots/bud-wordpress-theme-json/src/index.ts b/sources/@roots/bud-wordpress-theme-json/src/index.ts index ebcaa5bfc6..8155335431 100644 --- a/sources/@roots/bud-wordpress-theme-json/src/index.ts +++ b/sources/@roots/bud-wordpress-theme-json/src/index.ts @@ -2,15 +2,15 @@ import type * as Extension from '@roots/bud-wordpress-theme-json/extension' declare module '@roots/bud-framework' { interface Bud { - wpjson: Extension.Api + wpjson: Extension.WordPressThemeJson } interface Modules { '@roots/bud-tailwindcss-theme-json?': any - '@roots/bud-wordpress-theme-json': Extension.WordPressThemeJSON + '@roots/bud-wordpress-theme-json': Extension.WordPressThemeJson } } -export {WordPressThemeJSON as default} from '@roots/bud-wordpress-theme-json/extension' +export {WordPressThemeJson as default} from '@roots/bud-wordpress-theme-json/extension' export type {Schema as Theme} from '@roots/wordpress-theme-json-webpack-plugin' diff --git a/sources/@roots/wordpress-dependencies-webpack-plugin/src/plugin.ts b/sources/@roots/wordpress-dependencies-webpack-plugin/src/plugin.ts index dc25fbf75b..740dfcdab2 100644 --- a/sources/@roots/wordpress-dependencies-webpack-plugin/src/plugin.ts +++ b/sources/@roots/wordpress-dependencies-webpack-plugin/src/plugin.ts @@ -4,6 +4,8 @@ import type { } from '@roots/entrypoints-webpack-plugin' import type {Compilation} from 'webpack' +import {join} from 'node:path' + import {handle, wordpress} from '@roots/wordpress-transforms' import {bind} from 'helpful-decorators' import Webpack from 'webpack' @@ -144,6 +146,25 @@ export default class WordPressDependenciesWebpackPlugin { for (const request of requested) { if (this.options.exclude?.includes(request)) continue + /** + * It's harder to exclude react-refresh-runtime + * because it is often transitively included. + * + * So we check if the request includes the string + * and if it does, we check if it is in the exclude + * array. + * + * It is a bit of a hack, but it works. + */ + if ( + this.options?.exclude?.includes( + join(`react-refresh`, `runtime`), + ) && + request.includes(join(`react-refresh`, `runtime`)) + ) { + continue + } + if (!wordpress.isProvided(request)) continue const wordPressHandle = handle.transform(request) diff --git a/sources/@roots/wordpress-externals-webpack-plugin/src/plugin.ts b/sources/@roots/wordpress-externals-webpack-plugin/src/plugin.ts index 75c6526521..9b664c474a 100644 --- a/sources/@roots/wordpress-externals-webpack-plugin/src/plugin.ts +++ b/sources/@roots/wordpress-externals-webpack-plugin/src/plugin.ts @@ -1,5 +1,7 @@ import type {Options} from '@roots/wordpress-externals-webpack-plugin' +import {join} from 'node:path' + import {window} from '@roots/wordpress-transforms' import Webpack, {type WebpackPluginInstance} from 'webpack' @@ -25,6 +27,25 @@ export class WordPressExternalsWebpackPlugin // bail on excluded signifiers if (this.options?.exclude?.includes(request)) return callback() + /** + * It's harder to exclude react-refresh-runtime + * because it is often transitively included. + * + * So we check if the request includes the string + * and if it does, we check if it is in the exclude + * array. + * + * It is a bit of a hack, but it works. + */ + if ( + this.options?.exclude?.includes( + join(`react-refresh`, `runtime`), + ) && + request.includes(join(`react-refresh`, `runtime`)) + ) { + return callback() + } + const lookup = window.transform(request) return lookup ? callback(null, lookup) : callback() }).apply(compiler) diff --git a/sources/@roots/wordpress-theme-json-webpack-plugin/src/index.ts b/sources/@roots/wordpress-theme-json-webpack-plugin/src/index.ts index 0d1185cafc..136d3b955c 100644 --- a/sources/@roots/wordpress-theme-json-webpack-plugin/src/index.ts +++ b/sources/@roots/wordpress-theme-json-webpack-plugin/src/index.ts @@ -14,7 +14,7 @@ export interface Options extends SettingsAndStyles { /** * Warning comment about the file being generated. */ - __generated__?: string + __generated__?: false | string path: string version: 2 } diff --git a/sources/@roots/wordpress-theme-json-webpack-plugin/src/plugin.ts b/sources/@roots/wordpress-theme-json-webpack-plugin/src/plugin.ts index fca1798197..23f78bd8e3 100644 --- a/sources/@roots/wordpress-theme-json-webpack-plugin/src/plugin.ts +++ b/sources/@roots/wordpress-theme-json-webpack-plugin/src/plugin.ts @@ -58,6 +58,10 @@ export class ThemeJsonWebpackPlugin implements WebpackPluginInstance { */ public apply(compiler: Compiler) { if (!this.options.path) this.options.path = `../theme.json` + if (this.options.__generated__ === false) { + delete this.options.__generated__ + delete this.data.__generated__ + } compiler.hooks.thisCompilation.tap( this.constructor.name, diff --git a/tests/reproductions/issue-2126/theme.json b/tests/reproductions/issue-2126/theme.json index 33e7e628bd..7ad806e97f 100644 --- a/tests/reproductions/issue-2126/theme.json +++ b/tests/reproductions/issue-2126/theme.json @@ -1,7 +1,7 @@ { - "__generated__": "⚠️ This file is generated. Do not edit.", "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, + "__generated__": "⚠️ This file is generated. Do not edit.", "settings": { "color": { "custom": false, @@ -30,4 +30,4 @@ "dropCap": false } } -} \ No newline at end of file +}