Skip to content

Commit

Permalink
fix webpack 5 subapp HMR code injection
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip committed Mar 7, 2021
1 parent 7fdaa12 commit f202c0a
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 45 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 9 additions & 1 deletion packages/xarc-subapp/src/node/init-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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(" ");
Expand Down
20 changes: 16 additions & 4 deletions packages/xarc-subapp/src/node/init-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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");

Expand Down
1 change: 1 addition & 0 deletions packages/xarc-webpack/src/client/webpack5-jsonp-cdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion packages/xarc-webpack/src/partials/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(),
Expand Down
89 changes: 52 additions & 37 deletions packages/xarc-webpack/src/plugins/subapp-plugin-webpack5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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]) {
Expand Down Expand Up @@ -147,6 +157,8 @@ export class SubAppWebpackPlugin {
_makeIdentifierBEE: Function;
_tapAssets: Function;
_assetsFile: string;
_hasHmr: (compilation: any) => boolean;
_foundSubApps: string;

/**
*
Expand All @@ -162,9 +174,9 @@ export class SubAppWebpackPlugin {
*/
declareApiName?: string | string[];
/**
* Webpack version (4, 5, etc)
* Webpack version
*
* minimum 4
* minimum 5
*/
webpackVersion?: number;
/**
Expand All @@ -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() {
Expand All @@ -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)
};
}

Expand All @@ -234,26 +239,40 @@ 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]", {});
if (ast.callee.type === "Import" && arg.type === "Literal") {
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;
}
Expand All @@ -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());
});
}
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit f202c0a

Please sign in to comment.