From f202c0a171b82b3c187cde200b6d0d6791e07c56 Mon Sep 17 00:00:00 2001 From: Joel Chen Date: Sat, 6 Mar 2021 19:17:55 -0800 Subject: [PATCH] fix webpack 5 subapp HMR code injection --- .github/workflows/nodejs.yml | 4 +- packages/xarc-subapp/src/node/init-context.ts | 10 ++- packages/xarc-subapp/src/node/init-v2.ts | 20 ++++- .../src/client/webpack5-jsonp-cdn.ts | 1 + packages/xarc-webpack/src/partials/output.ts | 9 +- .../src/plugins/subapp-plugin-webpack5.ts | 89 +++++++++++-------- 6 files changed, 88 insertions(+), 45 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 9acdf0cd9..57077afa6 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -5,9 +5,9 @@ name: Node.js CI on: push: - branches: [master] + branches: [master, next-webpack-5] pull_request: - branches: [master] + branches: [master, next-webpack-5] jobs: build: diff --git a/packages/xarc-subapp/src/node/init-context.ts b/packages/xarc-subapp/src/node/init-context.ts index 27042b04a..4ed613a0e 100644 --- a/packages/xarc-subapp/src/node/init-context.ts +++ b/packages/xarc-subapp/src/node/init-context.ts @@ -3,6 +3,8 @@ import { InitProps } from "./types"; import { generateNonce } from "./utils"; +const isWebpackDev = Boolean(process.env.WEBPACK_DEV); + /** * Initialize all the up front code required for running subapps in the browser. * @@ -50,7 +52,13 @@ export function initContext(_setupContext: any, setupToken: Partial<{ props: Ini }; setCspNonce(context.user.scriptNonce, "script"); - setCspNonce(context.user.styleNonce, "style"); + // + // TODO: with Webpack 5 and mini-css-extract-plugin 1.x style HMR breaks when there's + // nonce enforcement so don't set style CSP nonce header. + // + if (!isWebpackDev) { + setCspNonce(context.user.styleNonce, "style"); + } if (cspValues.length > 0) { context.user.cspHeader = cspValues.join(" "); diff --git a/packages/xarc-subapp/src/node/init-v2.ts b/packages/xarc-subapp/src/node/init-v2.ts index c87496947..eae9e105f 100644 --- a/packages/xarc-subapp/src/node/init-v2.ts +++ b/packages/xarc-subapp/src/node/init-v2.ts @@ -35,6 +35,20 @@ ${cdnMapDataString} return { cdnUpdateScript, cdnAsJsonScript }; } +/** + * get @xarc/app major version to determine webpack 4 or 5 + * + * @returns @xarc/app major version + */ +function getXarcAppVersion() { + try { + // eslint-disable-next-line + return require("@xarc/app/package.json").version.split(".")[0]; + } catch { + return 9; + } +} + /** * Initialize common static assets such as xarcV2 client code, CDN data, and other JS bundles. * @@ -78,12 +92,10 @@ function initializeStaticAssets(props: InitProps) { const cdnMapData = cdnMap && (typeof cdnMap === "string" ? loadCdnMap(cdnMap) : cdnMap); - // eslint-disable-next-line - const xarcVer = require("@xarc/app/package.json").version.split(".")[0]; - // client side JS code required to start subapps and load assets // @xarc/app version 10 above use webpack 5 and no longer need webpack4Jsonp scripts - const webpack4JsonpJs = xarcVer < 10 ? getClientJs("webpack4-jsonp.js", "webpack4JsonP") : ""; + const webpack4JsonpJs = + getXarcAppVersion() < 10 ? getClientJs("webpack4-jsonp.js", "webpack4JsonP") : ""; const xarcV2Js = getClientJs("xarc-subapp-v2.js", "xarcV2Client"); const cdnMapScripts = !cdnMap ? "" : getClientJs("xarc-cdn-map.js", "xarcCdnMap"); diff --git a/packages/xarc-webpack/src/client/webpack5-jsonp-cdn.ts b/packages/xarc-webpack/src/client/webpack5-jsonp-cdn.ts index b5448202d..1b8cc6a79 100644 --- a/packages/xarc-webpack/src/client/webpack5-jsonp-cdn.ts +++ b/packages/xarc-webpack/src/client/webpack5-jsonp-cdn.ts @@ -7,6 +7,7 @@ * - https://github.com/webpack/webpack.js.org/pull/3033 */ +/* eslint-disable no-undef, @typescript-eslint/camelcase */ // @ts-nocheck function setup(w: any) { diff --git a/packages/xarc-webpack/src/partials/output.ts b/packages/xarc-webpack/src/partials/output.ts index 2781d495f..1dcbf6053 100644 --- a/packages/xarc-webpack/src/partials/output.ts +++ b/packages/xarc-webpack/src/partials/output.ts @@ -2,6 +2,7 @@ import * as Path from "path"; import { loadXarcOptions } from "../util/load-xarc-options"; +import * as mkdirp from "mkdirp"; module.exports = () => { const { babel, namespace } = loadXarcOptions(); @@ -27,9 +28,15 @@ module.exports = () => { } }; + const path = getOutputPath(); + + // karma 3.x uses fs.mkdirSync to create output and it fails if the output has multi dirs + // so create the output path here to help it get pass that. + mkdirp.sync(path); + return { output: { - path: getOutputPath(), + path, pathinfo: inspectpack, // Enable path information for inspectpack publicPath: "/js/", chunkFilename: getOutputFilename(), diff --git a/packages/xarc-webpack/src/plugins/subapp-plugin-webpack5.ts b/packages/xarc-webpack/src/plugins/subapp-plugin-webpack5.ts index b46f965d6..6ca917a0f 100644 --- a/packages/xarc-webpack/src/plugins/subapp-plugin-webpack5.ts +++ b/packages/xarc-webpack/src/plugins/subapp-plugin-webpack5.ts @@ -88,20 +88,28 @@ class SubAppHotAcceptDependency extends ModuleDependency { } } +const where = (source, loc) => { + return `${source}:${loc.start.line}:${loc.start.column + 1}`; +}; + +const noCwd = x => x.replace(process.cwd(), "."); + /** * subapp hot accept template, this will insert HMR code from ../client/hmr-accept.ts * into the module that declareSubApp. * */ class SubAppHotAcceptTemplate { - apply(dep: SubAppHotAcceptDependency, source: any, runtime: any) { + apply(dep: SubAppHotAcceptDependency, source: any, { runtimeTemplate, moduleGraph, chunkGraph }) { if (!(dep instanceof SubAppHotAcceptDependency)) { return; } - const content = runtime.moduleId({ - module: dep.module, - request: dep.request + const content = runtimeTemplate.moduleId({ + module: moduleGraph.getModule(dep), + chunkGraph, + request: dep.request, + weak: dep.weak }); const script = []; @@ -114,9 +122,11 @@ class SubAppHotAcceptTemplate { // exported and its toString to get the code. So it's important the // function is fully self contained without external dependencies. // + // TODO: update subapp-plugin to make module used? script.push(` /* subapp HMR accept */ -var __xarcHmr__ = (${hmrSetup.toString()})(window, module.hot);`); +var __xarcHmr__ = (${hmrSetup.toString()})(window, + (typeof __unused_webpack_module !== "undefined" ? __unused_webpack_module : module).hot);`); } if (!dep.injected[content]) { @@ -147,6 +157,8 @@ export class SubAppWebpackPlugin { _makeIdentifierBEE: Function; _tapAssets: Function; _assetsFile: string; + _hasHmr: (compilation: any) => boolean; + _foundSubApps: string; /** * @@ -162,9 +174,9 @@ export class SubAppWebpackPlugin { */ declareApiName?: string | string[]; /** - * Webpack version (4, 5, etc) + * Webpack version * - * minimum 4 + * minimum 5 */ webpackVersion?: number; /** @@ -177,24 +189,15 @@ export class SubAppWebpackPlugin { this._subApps = {}; this._webpackMajorVersion = webpackVersion; - const { makeIdentifierBEE, tapAssets } = this[`initWebpackVer${this._webpackMajorVersion}`](); + const { makeIdentifierBEE, tapAssets, hasHmr } = this[ + `initWebpackVer${this._webpackMajorVersion}` + ](); this._makeIdentifierBEE = makeIdentifierBEE; this._tapAssets = tapAssets; this._assetsFile = assetsFile; - } - - initWebpackVer4() { - const BEE = require("webpack/lib/BasicEvaluatedExpression"); - return { - BasicEvaluatedExpression: BEE, - makeIdentifierBEE: expr => { - return new BEE().setIdentifier(expr.name).setRange(expr.range); - }, - tapAssets: compiler => { - compiler.hooks.emit.tap(pluginName, compilation => this.updateAssets(compilation.assets)); - } - }; + this._hasHmr = hasHmr; + this._foundSubApps = ""; } initWebpackVer5() { @@ -211,7 +214,9 @@ export class SubAppWebpackPlugin { compiler.hooks.compilation.tap(pluginName, compilation => { compilation.hooks.processAssets.tap(pluginName, assets => this.updateAssets(assets)); }); - } + }, + // TODO: detect HMR from compilation + hasHmr: () => Boolean(process.env.WEBPACK_DEV) }; } @@ -234,11 +239,15 @@ export class SubAppWebpackPlugin { source: () => subapps, size: () => subapps.length }; - console.log("version 2 subapps found:", keys.join(", ")); // eslint-disable-line + const found = keys.join(", "); + if (this._foundSubApps !== found) { + this._foundSubApps = found; + console.log("version 2 subapps found:", found); // eslint-disable-line + } } } - findImportCall(ast) { + private findImportCall(ast, source) { switch (ast.type) { case "CallExpression": const arg = _.get(ast, "arguments[0]", {}); @@ -246,14 +255,24 @@ export class SubAppWebpackPlugin { return arg.value; } case "ReturnStatement": - return this.findImportCall(ast.argument); + return this.findImportCall(ast.argument, source); case "BlockStatement": for (const n of ast.body) { - const res = this.findImportCall(n); + const res = this.findImportCall(n, source); if (res) { return res; } } + // webpack 5 + case "ImportExpression": + assert( + ast.source.type === "Literal", + `${where( + noCwd(source), + ast.source.loc + )}: subapp module import must use literal string, got ${ast.source.type}` + ); + return ast.source.value; } return undefined; } @@ -270,11 +289,11 @@ export class SubAppWebpackPlugin { // It should not affect child compilations if (compilation.compiler !== compiler) return; - // const hotUpdateChunkTemplate = compilation.hotUpdateChunkTemplate; - // if (!hotUpdateChunkTemplate) return; + if (!this._hasHmr(compilation)) { + return; + } compilation.dependencyFactories.set(SubAppHotAcceptDependency, normalModuleFactory); - compilation.dependencyTemplates.set(SubAppHotAcceptDependency, new SubAppHotAcceptTemplate()); }); } @@ -320,12 +339,6 @@ export class SubAppWebpackPlugin { return parser[SHIM_parseCommentOptions](range); }; - const noCwd = x => x.replace(process.cwd(), "."); - - const where = (source, loc) => { - return `${source}:${loc.start.line}:${loc.start.column + 1}`; - }; - const parseForSubApp = (expression, apiName) => { const currentSource = _.get(parser, "state.current.resource", ""); const props = _.get(expression, "arguments[0].properties"); @@ -360,7 +373,9 @@ export class SubAppWebpackPlugin { // try to figure out the module that's being imported for this subapp // getModule function: () => import("./subapp-module") // getModule function: function () { return import("./subapp-module") } - const mod = this.findImportCall(gm); + const mod = this.findImportCall(gm, currentSource); + + assert(mod, `${cw()}: unable to find the request of the subapp's module import call`); this._subApps[nameVal] = { name: nameVal, @@ -371,7 +386,7 @@ export class SubAppWebpackPlugin { module: mod }; - if (process.env.WEBPACK_DEV && parser.state.compilation.hotUpdateChunkTemplate) { + if (this._hasHmr(parser.state.compilation)) { const dep = new SubAppHotAcceptDependency( mod, parser.state.module,