/* eslint-disable no-console, max-statements, global-require, @typescript-eslint/no-var-requires */
import Path from "path";
import { loadCdnMap, mapCdn, wrapStringFragment, urlJoin } from "./utils";
import { WebpackStats } from "./webpack-stats";
import Crypto from "crypto";
import { AssetPathMap, InitProps } from "./types";
import { SSR_PIPELINES, safeStringifyJson } from "./utils";
/**
* Initialize assets that namespace will isolate
*
* @param props - server template token props
* @param staticAssets - static assets
* @param namespace - name space
*
* @returns assets
*/
function initializeNamespaceAssets(staticAssets: any, namespace: string) {
const { cdnMapData, cdnMapDataString, randomId } = staticAssets;
let cdnAsJsonScript = "";
let cdnUpdateScript = "";
if (cdnMapData) {
const cdnMapJsonId = `${namespace}.cdn-map-${randomId}`;
cdnAsJsonScript = `
`;
cdnUpdateScript = `window.xarcV2.cdnUpdate({md:window.xarcV2.dyn("${cdnMapJsonId}")})
`;
}
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.
*
* @param props - server template token props
*
* @returns assets
*/
function initializeStaticAssets(props: InitProps) {
const isProd = process.env.NODE_ENV === "production";
const distDir = isProd
? Path.join(__dirname, "../../dist/min")
: Path.join(__dirname, "../../dist-browser~es5~cjs~/browser");
const getClientJs = (file: string, exportName: string) => {
const code = require(Path.join(distDir, file))[exportName].toString();
return `(${code})(window);
`;
};
const isWebpackDev = Boolean(process.env.WEBPACK_DEV);
const stats = new WebpackStats();
stats.load();
let pathMap: AssetPathMap;
let cdnMap;
if (isWebpackDev) {
if (props.devAssetData) {
pathMap = props.devAssetData.pathMap;
cdnMap = props.devAssetData.cdnMap;
}
} else if (props.prodAssetData) {
pathMap = props.prodAssetData.pathMap;
cdnMap = props.prodAssetData.cdnMap;
}
if (!pathMap) {
pathMap = { base: "/js" };
}
const cdnMapData = cdnMap && (typeof cdnMap === "string" ? loadCdnMap(cdnMap) : cdnMap);
// 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 =
getXarcAppVersion() < 10 ? getClientJs("webpack4-jsonp.js", "webpack4JsonP") : "";
const xarcV2Js = getClientJs("xarc-subapp-v2.js", "xarcV2Client");
const cdnMapScripts = !cdnMap ? "" : getClientJs("xarc-cdn-map.js", "xarcCdnMap");
const getFileAssetPath = (file: string) => {
const fromCdn = mapCdn(file, cdnMapData);
if (fromCdn !== false) {
return fromCdn;
}
const prefix = pathMap.base;
const ext = pathMap[Path.extname(file)] || "";
return urlJoin(prefix, ext, file);
};
const runtimeJsFiles = stats.getChunkAssetFilename("runtime", ".js");
const runtimeJsScripts = wrapStringFragment(
runtimeJsFiles
.map((file: string) => ``)
.join("\n"),
"",
"\n"
);
const mainJsFiles = stats.getChunkAssetFilename("main", ".js");
const mainJsScripts = wrapStringFragment(
mainJsFiles
.map((file: string) => ``)
.join("\n"),
"",
"\n"
);
// const mainCssFiles = stats.getChunkAssetFilename("main", ".css");
// const mainCssLink =
// mainCssFiles.length < 1
// ? ""
// : mainCssFiles.map(file => ``);
//
// TODO: only preload CSS for subapp chunks that has SSR
//
const allCssLinks = wrapStringFragment(
stats.allChunkNames
.map(chunkName => {
const links = stats.getChunkAssetFilename(chunkName, ".css").map(cssFile => {
return ``;
});
return wrapStringFragment(links.join(""), ``);
})
.filter(x => x)
.join("\n"),
"",
"\n\n"
);
return {
webpack4JsonpJs,
xarcV2Js,
cdnMapScripts,
cdnMapData,
cdnMapDataString: cdnMapData && safeStringifyJson(cdnMapData),
runtimeJsScripts,
mainJsScripts,
allCssLinks,
randomId: Crypto.randomBytes(8).toString("base64") // eslint-disable-line
};
}
/**
* Initialize all the up front code required for running subapps in the browser.
*
* @param setupContext - context for setup
* @param setupToken - token for setup
* @returns data with template process callback
*/
export function initSubApp(setupContext: any, setupToken: Partial<{ props: InitProps }>) {
const staticAssets = initializeStaticAssets(setupToken.props);
const {
allCssLinks,
xarcV2Js,
cdnMapScripts,
webpack4JsonpJs,
runtimeJsScripts,
mainJsScripts
} = staticAssets;
return {
process(context) {
const { request, scriptNonceAttr, styleNonceAttr, namespace } = context.user;
const { cdnAsJsonScript, cdnUpdateScript } = initializeNamespaceAssets(
staticAssets,
namespace
);
request[SSR_PIPELINES] = [];
const addScriptNonce = (text: string) => {
return !scriptNonceAttr
? text && text.replace(/{{SCRIPT_NONCE}}/g, "")
: text && text.replace(/{{SCRIPT_NONCE}}/g, context.user.scriptNonceAttr);
};
const addStyleNonce = (text: string) => {
return !styleNonceAttr
? text && text.replace(/{{STYLE_NONCE}}/g, "")
: text && text.replace(/{{STYLE_NONCE}}/g, context.user.styleNonceAttr);
};
return `
${addStyleNonce(allCssLinks)}${addScriptNonce(cdnAsJsonScript)}
${addScriptNonce(runtimeJsScripts)}${addScriptNonce(mainJsScripts)}`;
}
};
}