UNPKG

@lynx-js/template-webpack-plugin

Version:

Simplifies creation of Lynx template files to serve your webpack bundles

240 lines 9.75 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 { LynxTemplatePlugin } from './LynxTemplatePlugin.js'; /** * LynxEncodePlugin * * @public */ export class LynxEncodePlugin { options; /** * The stage of the beforeEncode hook. */ static BEFORE_ENCODE_STAGE = 256; /** * The stage of the encode hook. */ static ENCODE_STAGE = 256; /** * The stage of the beforeEmit hook. */ static BEFORE_EMIT_STAGE = 256; constructor(options) { this.options = options; } /** * `defaultOptions` is the default options that the {@link LynxEncodePlugin} uses. * * @example * `defaultOptions` can be used to change part of the option and keep others as the default value. * * ```js * // webpack.config.js * import { LynxEncodePlugin } from '@lynx-js/template-webpack-plugin' * export default { * plugins: [ * new LynxEncodePlugin({ * ...LynxEncodePlugin.defaultOptions, * enableRemoveCSSScope: true, * }), * ], * } * ``` * * @public */ static defaultOptions = Object .freeze({ inlineScripts: true, }); /** * The entry point of a webpack plugin. * @param compiler - the webpack compiler */ apply(compiler) { new LynxEncodePluginImpl(compiler, Object.assign({}, LynxEncodePlugin.defaultOptions, this.options)); } } export class LynxEncodePluginImpl { name = 'LynxEncodePlugin'; constructor(compiler, options) { this.options = options; const isDev = process.env['NODE_ENV'] === 'development' || compiler.options.mode === 'development'; compiler.hooks.thisCompilation.tap(this.name, compilation => { const templateHooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation); const inlinedAssets = new Set(); const { Compilation } = compiler.webpack; compilation.hooks.processAssets.tap({ name: this.name, // `PROCESS_ASSETS_STAGE_REPORT` is the last stage of the `processAssets` hook. // We need to run our asset deletion after this stage to ensure all assets have been processed. // E.g.: upload source-map to sentry. stage: Compilation.PROCESS_ASSETS_STAGE_REPORT + 1, }, () => { inlinedAssets.forEach((name) => { compilation.deleteAsset(name); }); inlinedAssets.clear(); }); templateHooks.beforeEncode.tapPromise({ name: this.name, stage: LynxEncodePlugin.BEFORE_ENCODE_STAGE, }, async (args) => { const { encodeData } = args; const { manifest } = encodeData; const [inlinedManifest, externalManifest] = Object.entries(manifest) .reduce(([inlined, external], [name, content]) => { const assert = compilation.getAsset(name); let chunk = null; for (const c of compilation.chunks) { if (c.files.has(name)) { chunk = c; break; } } let shouldInline = true; if (!chunk?.hasRuntime()) { shouldInline = this.#shouldInlineScript(name, assert.source.size()); } if (shouldInline) { inlined[name] = content; } else { external[name] = content; } return [inlined, external]; }, [{}, {}]); let publicPath = '/'; if (typeof compilation?.outputOptions.publicPath === 'function') { compilation.errors.push(new compiler.webpack.WebpackError('`publicPath` as a function is not supported yet.')); } else { publicPath = compilation?.outputOptions.publicPath ?? '/'; } if (!isDebug() && !isDev && !isRsdoctor()) { [ encodeData.lepusCode.root, ...encodeData.lepusCode.chunks, ...Object.keys(inlinedManifest).map(name => ({ name })), ...encodeData.css.chunks, ] .filter(asset => asset !== undefined) .forEach(asset => inlinedAssets.add(asset.name)); } encodeData.manifest = { // `app-service.js` is the entry point of a template. // All the initial chunks will be loaded **synchronously**. // // ``` // manifest: { // '/app-service.js': ` // lynx.requireModule('async-chunk1') // lynx.requireModule('async-chunk2') // lynx.requireModule('inlined-initial-chunk1') // lynx.requireModule('inlined-initial-chunk2') // lynx.requireModuleAsync('external-initial-chunk1') // lynx.requireModuleAsync('external-initial-chunk2') // `, // 'inlined-initial-chunk1': `<content>`, // 'inlined-initial-chunk2': `<content>`, // }, // ``` '/app-service.js': [ this.#appServiceBanner(), this.#appServiceContent(externalManifest, inlinedManifest, publicPath), this.#appServiceFooter(), ].join(''), ...Object.fromEntries(Object.entries(inlinedManifest).map(([name, content]) => [ this.#formatJSName(name, '/'), content, ])), }; return args; }); templateHooks.encode.tapPromise({ name: this.name, stage: LynxEncodePlugin.ENCODE_STAGE, }, async (args) => { const { encodeOptions } = args; const { getEncodeMode } = await import('@lynx-js/tasm'); const encode = getEncodeMode(); const { buffer, lepus_debug } = await Promise.resolve(encode(encodeOptions)); return { buffer, debugInfo: lepus_debug }; }); }); } #APP_SERVICE_NAME = '/app-service.js'; #appServiceBanner() { const loadScriptBanner = `(function(){'use strict';function n({tt}){`; const amdBanner = `tt.define('${this.#APP_SERVICE_NAME}',function(e,module,_,i,l,u,a,c,s,f,p,d,h,v,g,y,lynx){`; return loadScriptBanner + amdBanner; } #appServiceContent(externalManifest, inlinedManifest, publicPath) { const parts = []; const externalKeys = Object.keys(externalManifest); if (externalKeys.length > 0) { const externalRequires = externalKeys .map(name => `lynx.requireModuleAsync(${JSON.stringify(this.#formatJSName(name, publicPath))})`) .join(','); parts.push(externalRequires, ';'); } const inlinedKeys = Object.keys(inlinedManifest); if (inlinedKeys.length > 0) { parts.push('module.exports='); const inlinedRequires = inlinedKeys .map(name => `lynx.requireModule(${JSON.stringify(this.#formatJSName(name, '/'))},globDynamicComponentEntry?globDynamicComponentEntry:'__Card__')`) .join(','); parts.push(inlinedRequires, ';'); } return parts.join(''); } #appServiceFooter() { const loadScriptFooter = `}return{init:n}})()`; const amdFooter = `});return tt.require('${this.#APP_SERVICE_NAME}');`; return amdFooter + loadScriptFooter; } #formatJSName(name, publicPath) { const base = !publicPath || publicPath === 'auto' ? '/' : publicPath; const prefixed = base.endsWith('/') ? base : `${base}/`; const trimmed = name.startsWith('/') ? name.slice(1) : name; return `${prefixed}${trimmed}`; } #shouldInlineScript(name, size) { const inlineConfig = this.options.inlineScripts; if (inlineConfig instanceof RegExp) { return inlineConfig.test(name); } if (typeof inlineConfig === 'function') { return inlineConfig({ size, name }); } if (typeof inlineConfig === 'object') { if (inlineConfig.enable === false) return false; if (inlineConfig.test instanceof RegExp) { return inlineConfig.test.test(name); } return inlineConfig.test({ size, name }); } return inlineConfig !== false; } options; } export function isDebug() { if (!process.env['DEBUG']) { return false; } const values = process.env['DEBUG'].toLocaleLowerCase().split(','); return [ 'rspeedy', '*', 'rspeedy:*', 'rspeedy:template', ].some((key) => values.includes(key)); } export function isRsdoctor() { return process.env['RSDOCTOR'] === 'true'; } //# sourceMappingURL=LynxEncodePlugin.js.map