@lynx-js/react-webpack-plugin
Version:
A webpack plugin for ReactLynx
204 lines • 9.06 kB
JavaScript
// 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