tpa-style-webpack-plugin
Version:
A Webpack plugin that handles wix tpa styles, it separates static css file that injects dynamic style at runtime.
223 lines (220 loc) • 11 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const webpack_sources_1 = __importDefault(require("webpack-sources"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const postcss_1 = __importDefault(require("postcss"));
const postcss_extract_styles_1 = __importDefault(require("postcss-extract-styles"));
const postcssPlugin_1 = require("./postcssPlugin");
const postcss_prefix_selector_1 = __importDefault(require("postcss-prefix-selector"));
const crypto_1 = require("crypto");
const webpack_1 = __importDefault(require("webpack"));
const standaloneCssConfigFilename_1 = require("./standaloneCssConfigFilename");
const isWebpack5 = parseInt(webpack_1.default.version, 10) === 5;
// use webpack's `webpack-sources` version, if it's v5, we'll get v2.0.0
const { RawSource, ReplaceSource } = isWebpack5 ? webpack_1.default.sources : webpack_sources_1.default;
class TPAStylePlugin {
constructor(options) {
this._options = Object.assign({ pattern: [/"\w+\([^"]*\)"/, /START|END|DIR|STARTSIGN|ENDSIGN|DEG\-START|DEG\-END/], jsChunkPattern: /\.js$/, cssChunkPattern: /\.css$/ }, options);
const hash = this.getCompilationHash();
this.compilationHash = `__${hash.substr(0, 6)}__`;
}
getCompilationHash() {
if (isWebpack5 || this._options.packageName) {
return crypto_1.createHash('md5')
.update(this._options.packageName)
.digest('hex');
}
return crypto_1.createHash('md5')
.update(new Date().getTime().toString())
.digest('hex');
}
apply(compiler) {
const cheapModuleEvalSourceMap = isWebpack5 ? 'eval-cheap-module-source-map' : 'cheap-module-eval-source-map';
const shouldEscapeContent = [cheapModuleEvalSourceMap, 'cheap-eval-source-map'].includes(compiler.options.devtool);
this.replaceRuntimeModule(compiler);
compiler.hooks.compilation.tap(TPAStylePlugin.pluginName, compilation => {
const pluginDescriptor = isWebpack5
? {
name: TPAStylePlugin.pluginName,
stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
}
: TPAStylePlugin.pluginName;
const hook = isWebpack5 ? compilation.hooks.processAssets : compilation.hooks.optimizeChunkAssets;
hook.tapAsync(pluginDescriptor, (chunks, callback) => {
const actualChunks = isWebpack5 ? compilation.chunks : chunks;
this.extract(compilation, actualChunks)
.then(extractResults => this.replaceSource(compilation, extractResults, shouldEscapeContent))
.then(() => callback())
.catch(callback);
});
});
}
replaceRuntimeModule(compiler) {
const runtimePath = path_1.default.resolve(__dirname, '../../runtime.js');
const nmrp = new webpack_1.default.NormalModuleReplacementPlugin(/runtime\.js$/, resource => {
if (isWebpack5) {
// `resource`, `request`, and `loaders` are exposed under `createData`
// in webpack v5
resource = resource.createData;
}
if (fs_1.default.realpathSync(resource.resource) !== runtimePath) {
return;
}
const dirname = path_1.default.dirname(resource.resource);
resource.request = './dist/runtime/main.js';
resource.resource = path_1.default.join(dirname, 'dist/runtime/main.js');
resource.loaders.push({
loader: path_1.default.join(__dirname, 'runtimeLoader.js'),
options: JSON.stringify({ compilationHash: this.compilationHash }),
});
});
nmrp.apply(compiler);
}
extract(compilation, chunks) {
const promises = [];
chunks.forEach(chunk => {
// webpack 5 turned this from an array to a set
const files = isWebpack5 ? [...chunk.files] : chunk.files;
promises.push(...files
.filter(fileName => this._options.cssChunkPattern.test(fileName))
.map(cssFile => postcss_1.default([postcss_extract_styles_1.default(this._options)])
.use(postcss_prefix_selector_1.default({ prefix: this.compilationHash, exclude: [/^\w+/] }))
.process(compilation.assets[cssFile].source(), { from: cssFile, to: cssFile })
.then((result) => {
compilation.assets[cssFile] = new RawSource(result.css.replace(new RegExp(`${this.compilationHash} `, 'g'), ''));
return new Promise(resolve => {
postcss_1.default([
postcss_prefix_selector_1.default({
prefix: this.compilationHash,
}),
postcssPlugin_1.extractTPACustomSyntax({
onFinish: ({ cssVars, customSyntaxStrs, css }) => {
resolve({ chunk, cssVars, customSyntaxStrs, css, staticCss: result.css });
},
}),
]).process(result.extracted, { from: undefined }).css;
});
})));
});
return Promise.all(promises);
}
getPlaceholderContent(params, shouldEscapeContent) {
if ('staticCss' in params) {
params.staticCssHash = crypto_1.createHash('sha1')
.update(params.staticCss)
.digest('base64');
}
const content = JSON.stringify(params);
if (!shouldEscapeContent) {
return content;
}
const escapedContent = JSON.stringify(content);
return escapedContent.substring(1, escapedContent.length - 1);
}
getStyleParamsDefaultsContent(assets, fileName) {
const styleParamsFileName = standaloneCssConfigFilename_1.getRelatedStyleParamsFileName(fileName);
const styleParamsFile = assets[styleParamsFileName];
if (styleParamsFile) {
return `(function () {
var styleParamsModule = {};
var styleParamsExports = {};
(function (module, exports) {
${styleParamsFile.source()}
})(styleParamsModule, styleParamsExports);
return styleParamsModule.exports && styleParamsModule.exports.default ? styleParamsModule.exports.default : null;
})()`;
}
return null;
}
replaceByPlaceHolder({ sourceCode, newSource, shouldEscapeContent, placeholder, params }) {
const placeHolder = `'${this.compilationHash}${placeholder}'`;
const placeHolderPos = sourceCode.indexOf(placeHolder);
if (placeHolderPos > -1) {
newSource.replace(placeHolderPos, placeHolderPos + placeHolder.length - 1, this.getPlaceholderContent(params, shouldEscapeContent));
}
}
generateStandaloneCssConfig({ shouldEscapeContent, params, defaults }) {
const sourceCode = fs_1.default.readFileSync(path_1.default.join(__dirname, './cssConfigTemplate.js')).toString();
return new RawSource(sourceCode
.replace(`'CSS_CONFIG_PLACEHOLDER'`, this.getPlaceholderContent(params, shouldEscapeContent))
.replace(`"STYLE_PARAMS_DEFAULTS_PLACEHOLDER"`, defaults));
}
replaceSource(compilation, extractResults, shouldEscapeContent) {
const entryMergedChunks = this.getEntryMergedChunks(extractResults);
entryMergedChunks.forEach(({ chunk, cssVars, customSyntaxStrs, css, staticCss }) => {
// webpack 5 turned this from an array to a set
const files = isWebpack5 ? [...chunk.files] : chunk.files;
files
.filter(fileName => this._options.jsChunkPattern.test(fileName))
.forEach(file => {
const sourceCode = compilation.assets[file].source();
const newSource = new ReplaceSource(compilation.assets[file], file);
this.replaceByPlaceHolder({
sourceCode,
newSource,
shouldEscapeContent,
placeholder: 'INJECTED_DATA_PLACEHOLDER',
params: {
cssVars,
customSyntaxStrs,
css,
},
});
this.replaceByPlaceHolder({
sourceCode,
newSource,
shouldEscapeContent,
placeholder: 'INJECTED_STATIC_DATA_PLACEHOLDER',
params: {
staticCss,
},
});
const cssConfigFilename = standaloneCssConfigFilename_1.generateStandaloneCssConfigFilename(file);
const defaults = this.getStyleParamsDefaultsContent(compilation.assets, file);
const params = {
cssVars,
customSyntaxStrs,
css,
staticCss,
compilationHash: this.compilationHash,
defaults: 'STYLE_PARAMS_DEFAULTS_PLACEHOLDER',
};
compilation.assets[cssConfigFilename] = this.generateStandaloneCssConfig({
shouldEscapeContent,
params,
defaults,
});
compilation.assets[file] = newSource;
});
});
}
getEntryMergedChunks(extractResults) {
const entryMergedChunks = extractResults
.filter(({ chunk }) => chunk.canBeInitial())
.reduce((chunkMap, currentResult) => {
const currentChunk = currentResult.chunk;
if (chunkMap.hasOwnProperty(currentChunk.id)) {
chunkMap[currentChunk.id] = this.mergeExtractResults(chunkMap[currentChunk.id], currentResult);
}
else {
chunkMap[currentChunk.id] = currentResult;
}
return chunkMap;
}, {});
return Object.keys(entryMergedChunks).map(key => entryMergedChunks[key]);
}
mergeExtractResults(extractResult1, extractResult2) {
const newResult = Object.assign({}, extractResult1);
newResult.cssVars = Object.assign(Object.assign({}, newResult.cssVars), extractResult2.cssVars);
newResult.customSyntaxStrs = newResult.customSyntaxStrs.concat(extractResult2.customSyntaxStrs);
newResult.css += `\n${extractResult2.css}`;
newResult.staticCss += `\n${extractResult2.staticCss}`;
return newResult;
}
}
TPAStylePlugin.pluginName = 'tpa-style-webpack-plugin';
module.exports = TPAStylePlugin;