UNPKG

@lynx-js/react-webpack-plugin

Version:
204 lines 9.06 kB
// Copyright 2024 The Lynx Authors. All rights reserved. // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. import * as fs from 'node:fs'; import { createRequire } from 'node:module'; import invariant from 'tiny-invariant'; import { LynxTemplatePlugin } from '@lynx-js/template-webpack-plugin'; import { RuntimeGlobals } from '@lynx-js/webpack-runtime-globals'; import { LAYERS } from './layer.js'; import { createLynxProcessEvalResultRuntimeModule } from './LynxProcessEvalResultRuntimeModule.js'; const require = createRequire(import.meta.url); /** * ReactWebpackPlugin allows using ReactLynx with webpack * * @example * ```js * // webpack.config.js * import { ReactWebpackPlugin } from '@lynx-js/react-webpack-plugin' * export default { * plugins: [new ReactWebpackPlugin()], * } * ``` * * @public */ class ReactWebpackPlugin { /** * The loaders for ReactLynx. * * @remarks * Note that this loader will only transform JSX/TSX to valid JavaScript. * For `.tsx` files, the type annotations would not be eliminated. * You should use `babel-loader` or `swc-loader` to load TypeScript files. * * @example * ```js * // webpack.config.js * import { ReactWebpackPlugin, LAYERS } from '@lynx-js/react-webpack-plugin' * export default { * module: { * rules: [ * { * test: /\.tsx?$/, * layer: LAYERS.MAIN_THREAD, * use: ['swc-loader', ReactWebpackPlugin.loaders.MAIN_THREAD] * }, * { * test: /\.tsx?$/, * layer: LAYERS.BACKGROUND, * use: ['swc-loader', ReactWebpackPlugin.loaders.BACKGROUND] * }, * ], * }, * plugins: [new ReactWebpackPlugin()], * } * ``` * * @public */ static { this.loaders = { BACKGROUND: require.resolve('../lib/loaders/background.js'), MAIN_THREAD: require.resolve('../lib/loaders/main-thread.js'), }; } constructor(options) { this.options = options; } /** * `defaultOptions` is the default options that the {@link ReactWebpackPlugin} uses. * * @public */ static { this.defaultOptions = Object .freeze({ disableCreateSelectorQueryIncompatibleWarning: false, firstScreenSyncTiming: 'immediately', enableSSR: false, mainThreadChunks: [], extractStr: false, experimental_isLazyBundle: false, }); } /** * The entry point of a webpack plugin. * @param compiler - the webpack compiler */ apply(compiler) { const options = Object.assign({}, ReactWebpackPlugin.defaultOptions, this.options); const { BannerPlugin, DefinePlugin, EnvironmentPlugin } = compiler.webpack; if (!options.experimental_isLazyBundle) { new BannerPlugin({ // TODO: handle cases that do not have `'use strict'` banner: `'use strict';var globDynamicComponentEntry=globDynamicComponentEntry||'__Card__';`, raw: true, test: options.mainThreadChunks, }).apply(compiler); } new EnvironmentPlugin({ // Default values of null and undefined behave differently. // Use undefined for variables that must be provided during bundling, or null if they are optional. DEBUG: null, }).apply(compiler); new DefinePlugin({ __DEV__: JSON.stringify(compiler.options.mode === 'development'), // We enable profile by default in development. // It can also be disabled by environment variable `REACT_PROFILE=false` __PROFILE__: JSON.stringify(process.env['REACT_PROFILE'] ?? compiler.options.mode === 'development'), __EXTRACT_STR__: JSON.stringify(Boolean(options.extractStr)), __FIRST_SCREEN_SYNC_TIMING__: JSON.stringify(options.firstScreenSyncTiming), __ENABLE_SSR__: JSON.stringify(options.enableSSR), __DISABLE_CREATE_SELECTOR_QUERY_INCOMPATIBLE_WARNING__: JSON.stringify(options.disableCreateSelectorQueryIncompatibleWarning), }).apply(compiler); compiler.hooks.thisCompilation.tap(this.constructor.name, compilation => { const onceForChunkSet = new WeakSet(); compilation.hooks.runtimeRequirementInTree.for(compiler.webpack.RuntimeGlobals.ensureChunkHandlers).tap('ReactWebpackPlugin', (_, runtimeRequirements) => { runtimeRequirements.add(RuntimeGlobals.lynxProcessEvalResult); }); compilation.hooks.runtimeRequirementInTree.for(RuntimeGlobals.lynxProcessEvalResult).tap('ReactWebpackPlugin', (chunk) => { if (onceForChunkSet.has(chunk)) { return; } onceForChunkSet.add(chunk); if (chunk.name?.includes(':background')) { return; } const LynxProcessEvalResultRuntimeModule = createLynxProcessEvalResultRuntimeModule(compiler.webpack); compilation.addRuntimeModule(chunk, new LynxProcessEvalResultRuntimeModule()); }); compilation.hooks.processAssets.tap({ name: this.constructor.name, stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, }, () => { for (const name of options.mainThreadChunks ?? []) { this.#updateMainThreadInfo(compilation, name); } compilation.chunkGroups // Async ChunkGroups .filter(cg => !cg.isInitial()) // MainThread ChunkGroups .filter(cg => cg.origins.every(origin => origin.module?.layer === LAYERS.MAIN_THREAD)) .forEach(cg => { const files = cg.getFiles(); files .filter(name => name.endsWith('.js')) .forEach(name => this.#updateMainThreadInfo(compilation, name)); }); }); // TODO: replace LynxTemplatePlugin types with Rspack // @ts-expect-error Rspack x Webpack compilation not match const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation); const { RawSource, ConcatSource } = compiler.webpack.sources; hooks.beforeEncode.tap(this.constructor.name, (args) => { const lepusCode = args.encodeData.lepusCode; if (lepusCode.root?.source.source().toString()?.includes('registerWorkletInternal')) { const path = compiler.options.mode === 'development' ? '@lynx-js/react/worklet-dev-runtime' : '@lynx-js/react/worklet-runtime'; const runtimeFile = require.resolve(path); lepusCode.chunks.push({ name: 'worklet-runtime', source: new RawSource(fs.readFileSync(runtimeFile, 'utf8')), info: { ['lynx:main-thread']: true, }, }); } return args; }); // Inject `module.exports` for async main-thread chunks hooks.beforeEncode.tap(this.constructor.name, (args) => { const { encodeData } = args; // A lazy bundle may not have main-thread code if (!encodeData.lepusCode.root) { return args; } if (encodeData.sourceContent.appType === 'card') { return args; } // We inject `module.exports` for each async template. compilation.updateAsset(encodeData.lepusCode.root.name, (old) => new ConcatSource(`\ (function (globDynamicComponentEntry) { const module = { exports: {} } const exports = module.exports `, old, ` ;return module.exports })`)); return args; }); // The react-transform will add `-${LAYER}` to the webpackChunkName. // We replace it with an empty string here to make sure main-thread & background chunk match. hooks.asyncChunkName.tap(this.constructor.name, (chunkName) => chunkName ?.replaceAll(`-${LAYERS.BACKGROUND}`, '') ?.replaceAll(`-${LAYERS.MAIN_THREAD}`, '')); }); } #updateMainThreadInfo(compilation, name) { const asset = compilation.getAsset(name); invariant(asset, `Should have main thread asset ${name}`); compilation.updateAsset(asset.name, asset.source, { ...asset.info, 'lynx:main-thread': true, }); } } export { ReactWebpackPlugin as ReactWebpackPlugin }; //# sourceMappingURL=ReactWebpackPlugin.js.map