Build: Vite and webpack
What the build has to do
Whatever the tool, the output must satisfy the Drupal side:
- One bundle per component, named after the component folder, written to
assets/(assets/<name>.js,assets/<name>.css), because the component'slibraryOverridespoints there. - React and ReactDOM are externals mapped to the globals
React/ReactDOM, and the UMD builds are copied toassets/react/(libraryreact_scaffold/react). - Shared helpers (
helpers.jsx,apiClient.js,common.js) are built as their own entries and loaded by libraries, not bundled into components. - CSS is extracted from the
import './x.scss'in the entry. - No JS dependency on jQuery or on Drupal internals except the globals
Drupal,once,drupalSettings.
Vite (what the scaffold uses)
vite.config.js highlights:
js
// entries: one per folder in components/ that has index.jsx (or index.js)
const entries = { common, helpers, apiClient, ...componentFolders };
build: {
outDir: 'assets',
emptyOutDir: true,
rollupOptions: {
input: entries,
external: ['react', 'react-dom'],
output: { entryFileNames: '[name].js', chunkFileNames: '[name].js', assetFileNames: '[name][extname]' },
},
},
plugins: [
react({ jsxRuntime: 'classic' }),
vitePluginExternal({ externals: { react: 'React', 'react-dom': 'ReactDOM', 'react-dom/client': 'ReactDOM' } }),
vitePluginCopyReact(mode), // copies node_modules/react/umd/*.js into assets/react/
],
esbuild: { jsxInject: `import React from 'react'` }, // classic runtime: React is the globalThings worth knowing:
- Output is ES modules. Rollup splits tiny shared chunks (
react.js,react-dom_client.js: shims for the externals). Scripts withimportstatements only run as modules, so every component library entry usesattributes: { type: module }and thehelperslibrary entry too. Module scripts are deferred and execute beforeDOMContentLoaded, which is when Drupal attaches behaviors, so ordering is fine. react-dom/clientis mapped toReactDOM: the UMD React 18ReactDOMobject hascreateRoot.jsxRuntime: 'classic'+jsxInject: JSX compiles toReact.createElementwith the global React.- Modes:
npm run watch= development (unminified React UMD, source maps),npm run dist= production. - New component = no config change: the dynamic entries read
components/. - CSS names: the CSS of
index.jsxis emitted asassets/<entry>.css.
Webpack (equivalent sketch)
Not tested in this repository
The scaffold ships Vite only. This is the same contract expressed for webpack 5, use it as a starting point.
js
// webpack.config.js
const path = require('path');
const fs = require('fs');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const components = fs.readdirSync('components', { withFileTypes: true })
.filter((d) => d.isDirectory() && fs.existsSync(`components/${d.name}/index.jsx`))
.reduce((acc, d) => ({ ...acc, [d.name]: `./components/${d.name}/index.jsx` }), {});
module.exports = (env, argv) => ({
entry: { common: './components/common.js', helpers: './components/helpers.jsx', apiClient: './components/apiClient.js', ...components },
output: { path: path.resolve(__dirname, 'assets'), filename: '[name].js', clean: true },
externals: { react: 'React', 'react-dom': 'ReactDOM', 'react-dom/client': 'ReactDOM' },
optimization: { splitChunks: false }, // one self-contained file per entry
module: {
rules: [
{ test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' }, // @babel/preset-react with runtime: 'classic'
{ test: /\.s?css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
],
},
resolve: { extensions: ['.js', '.jsx'] },
plugins: [
new MiniCssExtractPlugin({ filename: '[name].css' }),
new CopyPlugin({ patterns: [
{ from: 'node_modules/react/umd/react.production.min.js', to: 'react/react.js' },
{ from: 'node_modules/react-dom/umd/react-dom.production.min.js', to: 'react/react-dom.js' },
] }),
],
});Because splitChunks: false yields classic scripts without import, drop attributes: { type: module } from the libraries. helpers.jsx uses import { forwardRef } from 'react', which the external mapping turns into the global React.forwardRef.
Checklist when a component does not show up
ls assets/has<name>.jsand<name>.css.- The path in
libraryOverridesmatches (../../assets/<name>.js), anddrush crwas run after editing the yml. - Page source contains the script, is it
type="module"? If the console says "Cannot use import statement outside a module", it is not. React,ReactDOM,apiClientare defined: the component library depends onreact_scaffold/react/react-api-client.- The mount element has the class your
once()selector uses.