Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding webpack plugins for subapp and dynamic import jsonp #1740

Merged
merged 1 commit into from
Sep 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/xarc-webpack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

export { initWebpackConfigComposer, generateConfig as compose } from "./util/generate-config";

export { JsonpScriptSrcPlugin } from "./plugins/jsonp-script-src-plugin";
export { SubAppWebpackPlugin } from "./plugins/subapp-plugin";

//
// When xrun execute a build task that involves invoking webpack it
// will check if user wants webpack to start with their webpack.config.js
Expand Down
45 changes: 45 additions & 0 deletions packages/xarc-webpack/src/plugins/jsonp-script-src-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const pluginName = "JsonpScriptSrcPlugin";
/**
* This plugin modifies the webpack bootstrap code generated by the plugin at
* webpack/lib/web/JsonpMainTemplatePlugin.js
*
* It will rename the function jsonpScriptSrc generated by that to webpackJsonpScriptSrc
* and install a new version that check a user provided function for a custom src.
*
* window.__webpack_get_script_src__(chunkId, publicPath, originalSrc)
*
* This is only for webpack 4 (tested with 4.43 and 4.44).
*
* Webpack 5 has official support for this https://github.com/webpack/webpack/pull/8462
* so it won't be necessary.
*/
export class JsonpScriptSrcPlugin {
constructor() {}

_applyMainTemplate(mainTemplate) {
// tapable/lib/Hook.js
// use stage 1 to ensure this executes after webpack/lib/web/JsonpMainTemplatePlugin.js
mainTemplate.hooks.localVars.tap({ name: pluginName, stage: 1 }, (source, chunk, hash) => {
// no jsonpScriptSrc function detected, do nothing
if (!source.includes("function jsonpScriptSrc")) {
return source;
}

const modSource = source.replace("function jsonpScriptSrc", "function webpackJsonpScriptSrc");
return `${modSource}

var userGetScriptSrc = window.__webpack_get_script_src__;
function jsonpScriptSrc(chunkId) {
var src = webpackJsonpScriptSrc(chunkId);
return (userGetScriptSrc && userGetScriptSrc(chunkId, ${mainTemplate.requireFn}.p, src)) || src;
}
`;
});
}

apply(compiler) {
compiler.hooks.thisCompilation.tap(pluginName, compilation => {
this._applyMainTemplate(compilation.mainTemplate);
});
}
}
231 changes: 231 additions & 0 deletions packages/xarc-webpack/src/plugins/subapp-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import Fs = require("fs");
import _ = require("lodash");
import Path = require("path");

const pluginName = "SubAppPlugin";

const findWebpackVersion = (): number => {
const webpackPkg = JSON.parse(
Fs.readFileSync(require.resolve("webpack/package.json")).toString()
);
const webpackVersion = parseInt(webpackPkg.version.split(".")[0]);
return webpackVersion;
};

const assert = (ok: boolean, fail: string | Function) => {
if (!ok) {
const x = typeof fail === "function" ? fail() : fail;
if (typeof x === "string") {
throw new Error(x);
}
throw x;
}
};

const SHIM_parseCommentOptions = Symbol("parseCommentOptions");

/**
* This plugin will look for `declareSubApp` calls and do these:
*
* 1. instruct webpack to name the dynamic import bundle as `subapp-<name>`
* 2. collect the subapp meta info and save them as `subapps.json`
*
*/
export class SubAppWebpackPlugin {
_declareApiName: string;
_subApps: Record<string, any>;
_wVer: number;
_makeIdentifierBEE: Function;
_tapAssets: Function;

/**
*
* @param options - subapp plugin options
*/
constructor({ declareApiName = "declareSubApp", webpackVersion = findWebpackVersion() } = {}) {
this._declareApiName = declareApiName;
this._subApps = {};
this._wVer = webpackVersion;

const { makeIdentifierBEE, tapAssets } = this[`initWebpackVer${this._wVer}`]();

this._makeIdentifierBEE = makeIdentifierBEE;
this._tapAssets = tapAssets;
}

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));
}
};
}

initWebpackVer5() {
const BEE = require("webpack/lib/javascript/BasicEvaluatedExpression");
return {
BasicEvaluatedExpression: BEE,
makeIdentifierBEE: expr => {
return new BEE()
.setIdentifier(expr.name, {}, () => [])
.setRange(expr.range)
.setExpression(expr);
},
tapAssets: compiler => {
compiler.hooks.compilation.tap(pluginName, compilation => {
compilation.hooks.processAssets.tap(pluginName, assets => this.updateAssets(assets));
});
}
};
}

updateAssets(assets) {
let subappMeta = {};
const keys = Object.keys(this._subApps);
if (keys.length > 0) {
subappMeta = keys.reduce(
(acc, k) => {
acc[k] = _.pick(this._subApps[k], ["name", "source", "module"]);
return acc;
},
{
"//about": "Subapp meta information collected during webpack compile"
}
);
}
const subapps = JSON.stringify(subappMeta, null, 2) + "\n";
assets["subapps.json"] = {
source: () => subapps,
size: () => subapps.length
};
}

findImportCall(ast) {
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);
case "BlockStatement":
for (const n of ast.body) {
const res = this.findImportCall(n);
if (res) {
return res;
}
}
}
return undefined;
}

apply(compiler) {
const apiName = this._declareApiName;

this._tapAssets(compiler);

const findGetModule = props => {
const prop = props.find(p => p.key.name === "getModule");
const funcBody = prop.value.body;
return funcBody;
};

compiler.hooks.normalModuleFactory.tap(pluginName, factory => {
factory.hooks.parser.for("javascript/auto").tap(pluginName, (parser, options) => {
parser[SHIM_parseCommentOptions] = parser.parseCommentOptions;

assert(
parser.parseCommentOptions,
`webpack parser doesn't have method 'parseCommentOptions' - not compatible with this plugin`
);

const xl = parser.parseCommentOptions.length;
assert(
xl === 1,
`webpack parser.parseCommentOptions takes ${xl} arguments - but expecting 1 so not compatible with this plugin`
);

parser.parseCommentOptions = range => {
for (const k in this._subApps) {
const subapp = this._subApps[k];
const gmod = subapp.getModule;
if (range[0] >= gmod.range[0] && gmod.range[1] >= range[1]) {
const name = subapp.name.toLowerCase().replace(/ /g, "_");
return {
options: { webpackChunkName: `subapp-${name}` },
errors: []
};
}
}
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}`;
};

parser.hooks.call.for(apiName).tap(pluginName, expression => {
const currentSource = _.get(parser, "state.current.resource", "");
const props = _.get(expression, "arguments[0].properties");
const cw = () => where(noCwd(currentSource), expression.loc);

assert(props, () => `${cw()}: you must pass an Object literal as argument to ${apiName}`);

const nameProp = props.find(p => p.key.name === "name");
assert(nameProp, () => `${cw()}: argument for ${apiName} doesn't have a name property`);

const nameVal = nameProp.value.value;
assert(
nameVal && typeof nameVal === "string",
() => `${cw()}: subapp name must be specified as an inlined literal string`
);
// the following breaks hot recompiling in dev mode
// const exist = this._subApps[nameVal];
// assert(
// !exist,
// () =>
// `${cw()}: subapp '${nameVal}' is already declared at ${where(
// noCwd(exist.source),
// exist.loc
// )}`
// );
const gm = findGetModule(props);

// 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") }
let mod = this.findImportCall(gm);

this._subApps[nameVal] = {
name: nameVal,
source: Path.relative(process.cwd(), currentSource),
loc: expression.loc,
range: expression.range,
getModule: gm,
module: mod
};
});

parser.hooks.evaluate
.for("Identifier")
.tap({ name: pluginName, before: "Parser" }, expression => {
if (expression.name === apiName) {
return this._makeIdentifierBEE(expression);
}

return undefined;
});
});
});
}
}

module.exports = SubAppWebpackPlugin;