Skip to content

Commit 48d1906

Browse files
committed
feat(configs): add /vite export with wordpressExternals; release 1.1.0
Adds the shared WordPress externals interop for the Vite 8 / Rolldown toolchain via a new ./vite export (wordpressExternals(command)): roots wordpressPlugin() + React/CJS-require shims for build, kucrut wp_scripts() for the dev server. Declares the Vite-8 toolchain packages as optional peer deps. Bumps to 1.1.0. Claude-Session: https://claude.ai/code/session_011LsRS3cybEkwuGfEpNET2y
1 parent 26480f1 commit 48d1906

3 files changed

Lines changed: 179 additions & 3 deletions

File tree

packages/webentor-configs/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Webentor Configs Changelog
22

3+
## 1.1.0
4+
5+
- Add `@webikon/webentor-configs/vite` export with `wordpressExternals(command)` — the shared
6+
WordPress externals interop for the Vite 8 / Rolldown toolchain. Picks the right strategy per
7+
command: roots `wordpressPlugin()` + React/CJS-require interop shims for `build`, kucrut
8+
`wp_scripts()` for the dev server (`serve`). Replaces the per-config copy of these shims in
9+
webentor-core and the theme.
10+
- Add `vite`, `@roots/vite-plugin`, `@vitejs/plugin-react`, `@kucrut/vite-for-wp` as optional
11+
peer dependencies (only required when using the `./vite` export).
12+
313
## 1.0.2
414

515
- Add `@webikon/webentor-core` to Prettier

packages/webentor-configs/package.json

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "@webikon/webentor-configs",
33
"homepage": "https://webikon.sk",
4-
"version": "1.0.2",
4+
"version": "1.1.0",
55
"author": "Webikon s.r.o.",
6-
"description": "Shared lint and formatting presets for Webentor projects.",
6+
"description": "Shared lint, formatting, and Vite presets for Webentor projects.",
77
"license": "MIT",
88
"maintainers": [
99
{
@@ -30,25 +30,43 @@
3030
"./eslint": "./eslint.config.js",
3131
"./stylelint": "./stylelint.config.js",
3232
"./prettier": "./prettier.config.js",
33+
"./vite": "./vite.js",
3334
"./phpcs.xml": "./phpcs.xml",
3435
"./bladeformatter": "./.bladeformatterrc",
3536
"./editorconfig": "./.editorconfig"
3637
},
3738
"peerDependencies": {
3839
"@eslint/js": "^9.0.0",
40+
"@kucrut/vite-for-wp": "^0.12.0",
41+
"@roots/vite-plugin": "^2.2.0",
42+
"@vitejs/plugin-react": "^6.0.0",
3943
"eslint": "^9.0.0",
4044
"eslint-config-prettier": "^10.0.0",
4145
"eslint-plugin-prettier": "^5.0.0",
4246
"eslint-plugin-react": "^7.0.0",
4347
"globals": "^16.0.0",
4448
"stylelint": "^16.0.0",
4549
"stylelint-config-recommended": "^17.0.0",
46-
"typescript-eslint": "^8.0.0"
50+
"typescript-eslint": "^8.0.0",
51+
"vite": "^8.0.0"
52+
},
53+
"peerDependenciesMeta": {
54+
"@kucrut/vite-for-wp": { "optional": true },
55+
"@roots/vite-plugin": { "optional": true },
56+
"@vitejs/plugin-react": { "optional": true },
57+
"vite": { "optional": true }
58+
},
59+
"devDependencies": {
60+
"@kucrut/vite-for-wp": "^0.12.0",
61+
"@roots/vite-plugin": "^2.2.0",
62+
"@vitejs/plugin-react": "^6.0.0",
63+
"vite": "^8.0.0"
4764
},
4865
"files": [
4966
"eslint.config.js",
5067
"stylelint.config.js",
5168
"prettier.config.js",
69+
"vite.js",
5270
"phpcs.xml",
5371
".bladeformatterrc",
5472
".editorconfig",

packages/webentor-configs/vite.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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

Comments
 (0)