import HtmlWebpackPlugin from 'html-webpack-plugin' import { Compiler } from 'webpack' import { addScriptTag } from '../utils' import log from '../log/index' import { initMediator, RENDER, SERVER } from './init' import { defaultOptions, getFreePort, getLocalIpAddress } from '../config' import { initMemoryFileSystem } from '../utils/MemoryFileSystem' import merge from 'lodash/merge' import fs from 'fs' import path from 'path' /** * @internal */ const PLUGIN_NAME = 'prerenderPlugin' /** * @internal */ const EVENT_LIST = process.env.NODE_ENV === 'production' ? ['watchClose', 'failed', 'done'] : ['watchClose', 'failed'] /** * @internal */ interface HtmlPluginData { html?: string outputName: string plugin: HtmlWebpackPlugin } class PrerenderPlugin { /** * @internal */ option: Options | undefined = undefined hasInit = false /** * @internal */ constructor(option: Options = {}) { this.option = option } /** * @internal */ apply(compiler: Compiler): void { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { const htmlWebpackPlugin = (compiler?.options?.plugins ?? []) .map(({ constructor }) => constructor) .find(({ name }) => name === 'HtmlWebpackPlugin') if (htmlWebpackPlugin) { if (compiler.hooks) { const htmlWebpackPluginBeforeHtmlProcessing = // @ts-ignore compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing || // @ts-ignore htmlWebpackPlugin.getHooks(compilation).beforeEmit htmlWebpackPluginBeforeHtmlProcessing.tapAsync( PLUGIN_NAME, async (htmlPluginData: HtmlPluginData, callback: Function) => { await this.init(this.option) if (process.env.NODE_ENV !== 'production') { htmlPluginData = this.injectJs(htmlPluginData) } callback(null, htmlPluginData) } ) } } }) compiler.hooks.afterEmit.tapAsync(PLUGIN_NAME, async (_compilation, callback: any) => { if (process.env.NODE_ENV === 'production') { await this.outputSkeletonScreen() } callback() this.hasInit = false }) EVENT_LIST.forEach(event => { // @ts-ignore compiler.hooks[event].tap(PLUGIN_NAME, () => { if (SERVER) { SERVER.destroy() this.hasInit = false } }) }) } /** * @internal */ private async init(option: Options = {}): Promise { try { if (this.hasInit) { return } const host = await getLocalIpAddress() const port = await getFreePort() const mergeOption = merge({ host, port }, defaultOptions, option) this.option = mergeOption await initMediator(this.option) initMemoryFileSystem() this.hasInit = true } catch (error) { log.error(error) this.hasInit = false } } /** * @internal */ private injectJs(htmlPluginData: HtmlPluginData): HtmlPluginData { const script = fs.readFileSync(path.join(__dirname, '../sock/sock_client.bundle.js')) const oldHtml = htmlPluginData.html htmlPluginData.html = addScriptTag( oldHtml as string, script.toString(), this?.option?.port ?? 8888 ) return htmlPluginData } /** * @internal */ private async outputSkeletonScreen() { await RENDER?.outputScreen() } } export default PrerenderPlugin