|
| 1 | +import { createHash } from 'node:crypto'; |
| 2 | +import { createRequire } from 'node:module'; |
| 3 | +import path from 'node:path'; |
| 4 | +import { wp_scripts } from '@kucrut/vite-for-wp/plugins'; |
| 5 | +import { wordpressPlugin } from '@roots/vite-plugin'; |
| 6 | +import react from '@vitejs/plugin-react'; |
| 7 | + |
| 8 | +// Shared WordPress externals interop for the Webentor Vite 8 / Rolldown toolchain. |
| 9 | +// |
| 10 | +// WordPress provides React and the @wordpress/* packages as browser globals (window.React, |
| 11 | +// window.wp.*), so the build externalizes them rather than bundling. Two toolchains handle this |
| 12 | +// differently, and each is correct only in one mode — so wordpressExternals() picks per command: |
| 13 | +// |
| 14 | +// build → @roots/vite-plugin's wordpressPlugin() (native Rolldown externals; sheds the |
| 15 | +// rollup-plugin-external-globals tax that kucrut's wp_scripts() carries) plus the two |
| 16 | +// shims below for Rolldown's CJS/React interop quirks. |
| 17 | +// serve → @kucrut/vite-for-wp's wp_scripts() (vite-plugin-external), which provides real dev |
| 18 | +// stash modules for @wordpress/* so CJS deps that `require('@wordpress/editor')` (e.g. |
| 19 | +// @10up/block-components) resolve, and serves react lazily. roots externalizes |
| 20 | +// uniformly with no dev-server story for those requires, and the build-only require |
| 21 | +// shim can't run in dev. |
| 22 | +// |
| 23 | +// Usage (in a theme/core vite.config.js): |
| 24 | +// import { wordpressExternals } from '@webikon/webentor-configs/vite'; |
| 25 | +// export default defineConfig(({ command }) => ({ |
| 26 | +// plugins: [tailwindcss(), v4wp({ ... }), ...wordpressExternals(command) /*, wordpressThemeJson() */], |
| 27 | +// })); |
| 28 | + |
| 29 | +const DEFAULT_EXTERNAL_MAPPINGS = { |
| 30 | + lodash: { global: ['lodash'], handle: 'lodash' }, |
| 31 | + moment: { global: ['moment'], handle: 'moment' }, |
| 32 | + jquery: { global: ['jQuery'], handle: 'jquery' }, |
| 33 | + backbone: { global: ['Backbone'], handle: 'backbone' }, |
| 34 | +}; |
| 35 | + |
| 36 | +const IDENT = /^[A-Za-z_$][\w$]*$/; |
| 37 | + |
| 38 | +// react / react-dom / react/jsx-runtime — served as virtual modules that re-export the |
| 39 | +// WP-provided window globals (window.React etc.). They must NOT be externalized: under Rolldown |
| 40 | +// that breaks `import * as React` (React$NN = React$NN → undefined → `useInsertionEffect` of |
| 41 | +// undefined). The module emits explicit ESM named re-exports — enumerated from the installed |
| 42 | +// package (same major WP ships) — so `import { useState }` resolves in both the Rolldown build |
| 43 | +// AND Vite's dev server. (A CJS `module.exports = window.React` works in the build via Rolldown |
| 44 | +// interop, but Vite dev can't synthesize named bindings from it — only a default — which broke |
| 45 | +// `import { useState } from 'react'` in the editor dev server.) Only used on the build path; in |
| 46 | +// dev wp_scripts() externalizes react via vite-plugin-external. |
| 47 | +function reactGlobalsShim() { |
| 48 | + const shims = { |
| 49 | + react: 'window.React', |
| 50 | + 'react-dom': 'window.ReactDOM', |
| 51 | + 'react/jsx-runtime': 'window.ReactJSXRuntime', |
| 52 | + }; |
| 53 | + const PREFIX = '\0wp-react-shim:'; |
| 54 | + return { |
| 55 | + name: 'wp-react-globals-shim', |
| 56 | + enforce: 'pre', |
| 57 | + resolveId(id) { |
| 58 | + if (Object.prototype.hasOwnProperty.call(shims, id)) { |
| 59 | + return PREFIX + id; |
| 60 | + } |
| 61 | + return null; |
| 62 | + }, |
| 63 | + load(id) { |
| 64 | + if (!id.startsWith(PREFIX)) return null; |
| 65 | + const pkg = id.slice(PREFIX.length); |
| 66 | + // Resolve the package from the consumer's working dir (the theme/core running Vite), so |
| 67 | + // the enumerated export names match the React version WordPress actually ships there. |
| 68 | + const require = createRequire(path.join(process.cwd(), 'vite.config.js')); |
| 69 | + const names = Object.keys(require(pkg)).filter( |
| 70 | + (n) => n !== 'default' && IDENT.test(n), |
| 71 | + ); |
| 72 | + return [ |
| 73 | + `const m = ${shims[pkg]};`, |
| 74 | + `export default m;`, |
| 75 | + ...names.map((n) => `export const ${n} = m[${JSON.stringify(n)}];`), |
| 76 | + ].join('\n'); |
| 77 | + }, |
| 78 | + }; |
| 79 | +} |
| 80 | + |
| 81 | +// Rolldown preserves CJS `require()` of *externalized* modules (e.g. @wordpress/icons does |
| 82 | +// require('react/jsx-runtime'); @10up/block-components does require('@wordpress/element')). The |
| 83 | +// browser has no `require`, so Rolldown's interop shim throws. We prepend a module-scoped |
| 84 | +// `require` that resolves the externalized modules from WP globals. |
| 85 | +// |
| 86 | +// This must happen in generateBundle (after Oxc minify) so the literal `require` identifier isn't |
| 87 | +// mangled — Vite 8/Rolldown drops build.rolldownOptions and offers no post-minify hook, so |
| 88 | +// banner/inject/renderChunk all either get stripped or minified. Because that's after the content |
| 89 | +// hash is computed, we recompute the hash and rename the chunk ourselves; Vite's manifest plugin |
| 90 | +// (enforce:'post') then emits the corrected filename. The shim is prepended without a trailing |
| 91 | +// newline so source-map line mapping is preserved. |
| 92 | +function wpExternalRequireShim() { |
| 93 | + const shim = |
| 94 | + 'var require=function(id){' + |
| 95 | + 'if(id.indexOf("@wordpress/")===0){var n=id.slice(11).replace(/-([a-z])/g,function(m,c){return c.toUpperCase()});return window.wp[n];}' + |
| 96 | + 'switch(id){' + |
| 97 | + 'case"lodash":return window.lodash;' + |
| 98 | + 'case"moment":return window.moment;' + |
| 99 | + 'case"jquery":return window.jQuery;' + |
| 100 | + 'case"backbone":return window.Backbone;' + |
| 101 | + '}throw new Error("Unmapped external require: "+id);};'; |
| 102 | + const NEEDLE = "doesn't expose the `require`"; |
| 103 | + return { |
| 104 | + name: 'wp-external-require-shim', |
| 105 | + apply: 'build', |
| 106 | + generateBundle(_options, bundle) { |
| 107 | + for (const file of Object.values(bundle)) { |
| 108 | + if (file.type !== 'chunk' || !file.code.includes(NEEDLE)) continue; |
| 109 | + file.code = shim + file.code; |
| 110 | + if (file.isEntry) { |
| 111 | + const hash = createHash('sha256') |
| 112 | + .update(file.code) |
| 113 | + .digest('base64url') |
| 114 | + .slice(0, 8); |
| 115 | + file.fileName = file.fileName.replace(/-[\w-]{8}\.js$/, `-${hash}.js`); |
| 116 | + } |
| 117 | + } |
| 118 | + }, |
| 119 | + }; |
| 120 | +} |
| 121 | + |
| 122 | +/** |
| 123 | + * WordPress externals plugin set for a Webentor Vite config, chosen per build command. |
| 124 | + * |
| 125 | + * @param {'build' | 'serve'} command Vite's resolved command (from `defineConfig(({command}) => …)`). |
| 126 | + * @param {object} [options] |
| 127 | + * @param {Record<string, {global: string[], handle: string}>} [options.externalMappings] |
| 128 | + * Extra non-@wordpress browser globals to externalize on the build path (defaults to |
| 129 | + * lodash/moment/jquery/backbone, matching kucrut's wp_scripts()). |
| 130 | + * @param {object} [options.react] Extra options merged into @vitejs/plugin-react (classic runtime). |
| 131 | + * @returns {import('vite').PluginOption[]} |
| 132 | + */ |
| 133 | +export function wordpressExternals(command, options = {}) { |
| 134 | + const { externalMappings = DEFAULT_EXTERNAL_MAPPINGS, react: reactOptions } = options; |
| 135 | + const reactPlugin = react({ jsxRuntime: 'classic', ...reactOptions }); |
| 136 | + |
| 137 | + if (command !== 'build') { |
| 138 | + // Dev server: kucrut handles @wordpress/* + react via vite-plugin-external dev stash modules. |
| 139 | + return [wp_scripts(), reactPlugin]; |
| 140 | + } |
| 141 | + |
| 142 | + return [ |
| 143 | + reactGlobalsShim(), |
| 144 | + wordpressPlugin({ jsx: false, externalMappings }), |
| 145 | + reactPlugin, |
| 146 | + wpExternalRequireShim(), |
| 147 | + ]; |
| 148 | +} |
0 commit comments