UNPKG

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
"use strict"; 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;