diff --git a/bin/packages/build-vendors.mjs b/bin/packages/build-vendors.mjs index 832a8e4ce1aefb..e8efe0ab45fd27 100755 --- a/bin/packages/build-vendors.mjs +++ b/bin/packages/build-vendors.mjs @@ -1,9 +1,5 @@ #!/usr/bin/env node -/** - * External dependencies - */ -import { copyFile, mkdir } from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import esbuild from 'esbuild'; @@ -13,112 +9,100 @@ const ROOT_DIR = path.resolve( __dirname, '../..' ); const BUILD_DIR = path.join( ROOT_DIR, 'build', 'scripts' ); const VENDORS_DIR = path.join( BUILD_DIR, 'vendors' ); -/** - * Copy React and ReactDOM UMD files from node_modules. - * These are pre-built browser-ready bundles that don't need processing. - */ -async function copyReactUMDFiles() { - console.log( 'šŸ“¦ Copying React UMD files...' ); - - const filesToCopy = [ - { - from: 'node_modules/react/umd/react.development.js', - to: 'react.js', - }, - { - from: 'node_modules/react/umd/react.production.min.js', - to: 'react.min.js', - }, - { - from: 'node_modules/react-dom/umd/react-dom.development.js', - to: 'react-dom.js', - }, - { - from: 'node_modules/react-dom/umd/react-dom.production.min.js', - to: 'react-dom.min.js', - }, - ]; - - await mkdir( VENDORS_DIR, { recursive: true } ); - - await Promise.all( - filesToCopy.map( ( { from, to } ) => { - const source = path.join( ROOT_DIR, from ); - const dest = path.join( VENDORS_DIR, to ); - return copyFile( source, dest ); - } ) - ); - - console.log( ' āœ” Copied React UMD files' ); -} +const VENDOR_SCRIPTS = [ + { + name: 'react', + global: 'React', + handle: 'react', + }, + { + name: 'react-dom', + global: 'ReactDOM', + handle: 'react-dom', + }, + { + name: 'react/jsx-runtime', + global: 'ReactJSXRuntime', + handle: 'react-jsx-runtime', + }, +]; /** - * Bundle react-jsx-runtime using esbuild. - * Creates a browser-ready bundle that exposes ReactJSXRuntime global. + * Bundle a vendor script from node_modules into an IIFE script. + * This is used to build packages like React that don't ship UMD builds. + * + * @param {Object} config Vendor script configuration. + * @param {string} config.name Package name (e.g., 'react', 'react-dom', 'react/jsx-runtime'). + * @param {string} config.global Global variable name (e.g., 'React', 'ReactDOM'). + * @param {string} config.handle WordPress script handle (e.g., 'react', 'react-dom'). + * @return {Promise} Promise that resolves when all builds are finished. */ -async function bundleReactJsxRuntime() { - console.log( 'šŸ“¦ Bundling react-jsx-runtime...' ); +async function bundleVendorScript( config ) { + const { name, global, handle } = config; - // Plugin to map React import to global window.React - const reactGlobalPlugin = { - name: 'react-global', + // Plugin that externalizes the `react` package. + const reactExternalPlugin = { + name: 'react-external', setup( build ) { - // Intercept imports of 'react' and provide window.React instead - build.onResolve( { filter: /^react$/ }, ( args ) => ( { - path: args.path, - namespace: 'react-global', - } ) ); - - build.onLoad( { filter: /.*/, namespace: 'react-global' }, () => ( { - contents: 'module.exports = window.React', - loader: 'js', - } ) ); + build.onResolve( { filter: /^react$/ }, ( args ) => { + if ( args.kind === 'entry-point' ) { + return null; + } + return { + path: 'react', + namespace: 'react-external', + }; + } ); + + build.onLoad( + { filter: /.*/, namespace: 'react-external' }, + () => ( { + contents: `module.exports = globalThis.React`, + loader: 'js', + } ) + ); }, }; - const baseConfig = { - entryPoints: [ 'react/jsx-runtime' ], - bundle: true, - format: 'iife', - globalName: 'ReactJSXRuntime', - target: 'es2015', - platform: 'browser', - plugins: [ reactGlobalPlugin ], - banner: { - js: '/* React JSX Runtime - https://react.dev/ */\n', - }, - }; - - await Promise.all( [ - // Development build - esbuild.build( { - ...baseConfig, - outfile: path.join( VENDORS_DIR, 'react-jsx-runtime.js' ), - minify: false, - } ), - // Production build - esbuild.build( { - ...baseConfig, - outfile: path.join( VENDORS_DIR, 'react-jsx-runtime.min.js' ), - minify: true, - } ), - ] ); - - console.log( ' āœ” Bundled react-jsx-runtime' ); + // Build both minified and non-minified versions + await Promise.all( + [ false, true ].map( ( production ) => { + const outputFile = handle + ( production ? '.min.js' : '.js' ); + return esbuild.build( { + entryPoints: [ name ], + outfile: path.join( VENDORS_DIR, outputFile ), + bundle: true, + format: 'iife', + globalName: global, + minify: production, + target: 'esnext', // Don't transpile, just bundle. + platform: 'browser', + plugins: [ reactExternalPlugin ], + } ); + } ) + ); } /** * Main build function. */ async function buildVendors() { - console.log( 'šŸ”Ø Building vendor files...\n' ); - - const startTime = Date.now(); - - await Promise.all( [ copyReactUMDFiles(), bundleReactJsxRuntime() ] ); - - const totalTime = Date.now() - startTime; - console.log( `\nšŸŽ‰ Vendor files built successfully! (${ totalTime }ms)` ); + console.log( '\nšŸ“¦ Bundling vendor scripts...\n' ); + + for ( const vendorConfig of VENDOR_SCRIPTS ) { + try { + const startTime = Date.now(); + await bundleVendorScript( vendorConfig ); + const buildTime = Date.now() - startTime; + console.log( + ` āœ” Bundled vendor ${ vendorConfig.name } (${ buildTime }ms)` + ); + } catch ( error ) { + console.error( + ` ✘ Failed to bundle vendor ${ vendorConfig.name }: ${ error.message }` + ); + } + } } buildVendors().catch( ( error ) => {