diff --git a/packages/js/components/webpack.config.js b/packages/js/components/webpack.config.js new file mode 100644 index 0000000000..18f12e769b --- /dev/null +++ b/packages/js/components/webpack.config.js @@ -0,0 +1,32 @@ +import path from 'node:path'; +import { default as defaultConfig } from '@wordpress/scripts/config/webpack.config.js'; +import { WebpackEmitAllPlugin } from '../../../tools/webpack/webpack-emit-all-plugin.js'; + +const dirname = new URL('.', import.meta.url).pathname; + +const plugins = [ + ...defaultConfig.plugins.filter( + (plugin) => plugin.constructor.name !== 'DependencyExtractionWebpackPlugin', + ), + new WebpackEmitAllPlugin({ context: path.join(dirname, 'src') }), +]; + +export default { + ...defaultConfig, + plugins, + devtool: 'source-map', + context: dirname, + entry: { index: path.join(dirname, 'src', 'index.ts') }, + output: { + ...defaultConfig.output, + path: path.join(dirname, 'build-module'), + environment: { module: true }, + library: { type: 'module' }, + }, + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react/jsx-runtime': 'ReactJSX', + }, + experiments: { outputModule: true }, +}; diff --git a/tools/webpack/webpack-emit-all-plugin.js b/tools/webpack/webpack-emit-all-plugin.js new file mode 100644 index 0000000000..6c1b76ca63 --- /dev/null +++ b/tools/webpack/webpack-emit-all-plugin.js @@ -0,0 +1,80 @@ +import { basename, dirname, join } from 'node:path'; + +/** + * Webpack plugin to emit all transpiled modules as individual files without bundling. + * This is useful for libraries, making them easier to consume and tree shake. + * + * Gutenberg solves this by custom scripting around Babel (which is rather complex). + * WooCommerce uses TypeScript (which doesn't work with Webpack plugins). + * + * See: + * https://github.com/webpack/webpack/issues/5866 + * https://github.com/webpack/webpack/issues/11230 + * + * Inspired by: https://github.com/DrewML/webpack-emit-all-plugin + */ +export class WebpackEmitAllPlugin { + constructor(opts = {}) { + this.context = opts.context; + this.ignorePattern = opts.ignorePattern || /node_modules/; + } + + apply(compiler) { + compiler.hooks.environment.tap('EmitAllPlugin', (args) => { + compiler.options.optimization = { + ...compiler.options.optimization, + minimize: false, + minimizer: [], + concatenateModules: false, + splitChunks: false, + realContentHash: false, + removeEmptyChunks: false, + }; + }); + + compiler.hooks.compilation.tap('EmitAllPlugin', (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'EmitAllPlugin', + stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + additionalAssets: true, + }, + () => { + Object.keys(compilation.assets).forEach((asset) => { + if (asset.endsWith('.js')) { + compilation.deleteAsset(asset); + } + }); + }, + ); + }); + + compiler.hooks.afterEmit.tapPromise('EmitAllPlugin', (compilation) => + Promise.all( + Array.from(compilation.modules).map((module) => + this.#processModule(compiler, module), + ), + ), + ); + } + + async #processModule(compiler, module) { + const fs = compiler.outputFileSystem.promises; + const outputPath = compiler.options.output.path; + const originalSource = module.originalSource(); + if (!originalSource || this.ignorePattern.test(module.resource)) { + return; + } + + const path = module.resource.replace(this.context ?? compiler.context, ''); + const dest = join(outputPath, path).replace(/\.[jt]sx?$/i, '.js'); + const { source, map } = originalSource.sourceAndMap(); + const suffix = map ? `\n//# sourceMappingURL=${basename(dest)}.map` : ''; + + await fs.mkdir(dirname(dest), { recursive: true }); + return Promise.all([ + fs.writeFile(dest, `${source}${suffix}`), + map ? fs.writeFile(`${dest}.map`, JSON.stringify(map)) : undefined, + ]); + } +}