@lynx-js/runtime-wrapper-webpack-plugin
Version:
Use runtime wrapper which allow JavaScript to be load by Lynx.
229 lines • 8.41 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 { RuntimeGlobals } from '@lynx-js/webpack-runtime-globals';
const defaultInjectVars = [
'Card',
'setTimeout',
'setInterval',
'clearInterval',
'clearTimeout',
'NativeModules',
'tt',
'console',
'Component',
'ReactLynx',
'nativeAppId',
'Behavior',
'LynxJSBI',
'lynx',
// BOM API
'window',
'document',
'frames',
'self',
'location',
'navigator',
'localStorage',
'history',
'Caches',
'screen',
'alert',
'confirm',
'prompt',
'fetch',
'XMLHttpRequest',
'__WebSocket__', // We would provide `WebSocket` using `ProvidePlugin`
'webkit',
'Reporter',
'print',
'global',
// Lynx API
'requestAnimationFrame',
'cancelAnimationFrame',
];
/**
* RuntimeWrapperWebpackPlugin adds runtime wrappers to JavaScript and allow to be loaded by Lynx.
*
* @public
*/
class RuntimeWrapperWebpackPlugin {
options;
constructor(options = {}) {
this.options = options;
}
/**
* `defaultOptions` is the default options that the {@link RuntimeWrapperWebpackPlugin} uses.
*
* @public
*/
static defaultOptions = Object.freeze({
targetSdkVersion: '3.2',
test: /\.js$/,
bannerType: () => 'script',
injectVars: defaultInjectVars,
experimental_isLazyBundle: false,
});
/**
* The entry point of a webpack plugin.
* @param compiler - the webpack compiler
*/
apply(compiler) {
new RuntimeWrapperWebpackPluginImpl(compiler, Object.assign({}, RuntimeWrapperWebpackPlugin.defaultOptions, this.options));
}
}
export { RuntimeWrapperWebpackPlugin };
const defaultInjectStr = defaultInjectVars.join(',');
class RuntimeWrapperWebpackPluginImpl {
compiler;
options;
name = 'RuntimeWrapperWebpackPlugin';
constructor(compiler, options) {
this.compiler = compiler;
this.options = options;
const { targetSdkVersion, test, experimental_isLazyBundle } = options;
const { BannerPlugin } = compiler.webpack;
const isDev = process.env['NODE_ENV'] === 'development'
|| compiler.options.mode === 'development';
let injectStr = defaultInjectStr;
if (typeof options.injectVars === 'function') {
injectStr = options.injectVars(defaultInjectVars).join(',');
}
const iife = compiler.options.output.iife ?? true;
// banner
new BannerPlugin({
test: test,
raw: true,
banner: ({ chunk, filename }) => {
const banner = this.#getBannerType(filename) === 'script'
? loadScriptBanner()
: loadBundleBanner();
return banner
+ amdBanner({
// TODO: config
injectStr,
overrideRuntimePromise: true,
moduleId: '[name].js',
targetSdkVersion,
iife,
})
// In standalone lazy bundle mode, the lazy bundle will
// also has chunk.id "main", it will be conflict with the
// consumer project.
// We disable it for standalone lazy bundle since we do not
// support HMR for standalone lazy bundle now.
+ (isDev && !experimental_isLazyBundle
? lynxChunkEntries(JSON.stringify(chunk.id))
: '');
},
}).apply(compiler);
// footer
new BannerPlugin({
test: test,
footer: true,
raw: true,
banner: ({ filename }) => {
const footer = this.#getBannerType(filename) === 'script'
? loadScriptFooter
: loadBundleFooter;
return amdFooter('[name].js', iife) + footer;
},
}).apply(compiler);
}
#getBannerType(filename) {
const bannerType = this.options.bannerType(filename);
if (bannerType !== 'bundle' && bannerType !== 'script') {
throw new Error(`The return value of the bannerType expects the 'bundle' or 'script', but received '${bannerType}'.`);
}
return bannerType;
}
}
function lynxChunkEntries(chunkId) {
return `// lynx chunks entries
if (!${RuntimeGlobals.lynxChunkEntries}) {
// Initialize once
${RuntimeGlobals.lynxChunkEntries} = {};
}
if (!${RuntimeGlobals.lynxChunkEntries}[${chunkId}]) {
${RuntimeGlobals.lynxChunkEntries}[${chunkId}] = globDynamicComponentEntry;
} else {
globDynamicComponentEntry = ${RuntimeGlobals.lynxChunkEntries}[${chunkId}];
}
`;
}
// This is Lynx js runtime code. It will wrap user code (e. app-service.js, chunk.js)
// with two function wrapper, the outer function provide some utility functions, such as
// * `define` objects that the frontend framework implementor can use to provide some global-looking variables
// that are actually specific to the module in the inner function wrapper, such as
// * The `module` and `exports` are reserved objects that used to export values from the module in cjs, Lynx
// use `requireModule`/`requireModuleAsync` & return object to do `require` / `export` staffs rather than them.
// * common JS API `setTimeout`, `setInterval`, `clearInterval`, `clearTimeout`, `NativeModules`, `console`,
// * `nativeAppId`, `lynx`, `LynxJSBI`
// Besides these, runtime code also contains some injected code to provide certain features.
// For example, the Promise object is replaced with a call to "getPromise" when `overrideRuntimePromise` is true.
const loadScriptBanner = (strictMode = true) => `(function(){
${strictMode ? '\'use strict\';' : ';'}
var g = (new Function('return this;'))();
function __init_card_bundle__(lynxCoreInject) {
g.__bundle__holder = undefined;
var globDynamicComponentEntry = g.globDynamicComponentEntry || '__Card__';
var tt = lynxCoreInject.tt;`;
const loadBundleBanner = (strictMode = true) => `(function(){
${strictMode ? '\'use strict\';' : ';'}
var eval2 = eval;
var g = eval2("this");
function initBundle(lynxCoreInject) {
var tt = lynxCoreInject.tt;
`;
const amdBanner = ({ injectStr, moduleId, overrideRuntimePromise, targetSdkVersion, iife, }) => {
const iifeWrapper = iife ? '' : `
// This needs to be wrapped in an IIFE because it needs to be isolated against Lynx injected variables.
(() => {`;
return (`
tt.define("${moduleId}", function(require, module, exports, ${injectStr}) {
lynx = lynx || {};
lynx.targetSdkVersion=lynx.targetSdkVersion||${JSON.stringify(targetSdkVersion)};
${overrideRuntimePromise ? `var Promise = lynx.Promise;` : ''}
fetch = fetch || lynx.fetch;
requestAnimationFrame = requestAnimationFrame || lynx.requestAnimationFrame;
cancelAnimationFrame = cancelAnimationFrame || lynx.cancelAnimationFrame;
${iifeWrapper}
`);
};
const amdFooter = (moduleId, iife) => `
${iife ? '' : '})();'}
});
return tt.require("${moduleId}");`;
// footer for app-service.js chunk
const loadScriptFooter = `
};`
// FYI: bundleModuleMode
// The loadScript behavior of app-service.js is determined by bundleModuleMode
// bundleModuleMode: 'EvalRequire' (default, unable to get the correct error stack.)
// ------------
// lynx_core.js
// const jsContent = nativeApp.readScript('app-service.js');
// eval(jsContent);
// -------------
// bundleModuleMode: 'ReturnByFunction' (most used since 2 yrs ago, 2020-11)
// ------------
// lynx_core.js
// const bundleInitReturnObj: BundleInitReturnObj = nativeApp.loadScript(appServiceName);
// if (bundleInitReturnObj && bundleInitReturnObj.init) {
// bundleInitReturnObj.init({ tt });
// }
// -------------
+ `
if (g && g.bundleSupportLoadScript){
var res = {init: __init_card_bundle__};
g.__bundle__holder = res;
return res;
} else {
__init_card_bundle__({"tt": tt});
};
})();
`;
const loadBundleFooter = `}
g.initBundle = initBundle;
})()`;
//# sourceMappingURL=RuntimeWrapperWebpackPlugin.js.map