Skip to content

Commit

Permalink
feat: add ParagonWebpackPlugin and clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz committed May 28, 2023
1 parent e832cbc commit 4745a57
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 66 deletions.
1 change: 1 addition & 0 deletions config/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module.exports = {
globals: {
newrelic: false,
PARAGON: false,
__webpack_hash__: false,
},
ignorePatterns: [
'module.config.js',
Expand Down
54 changes: 54 additions & 0 deletions config/data/paragonUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ function getParagonVersion(dir) {
return JSON.parse(fs.readFileSync(pathToPackageJson)).version;
}

/**
* TODO
*/
function getParagonThemeCss(dir) {
const pathToCoreCss = `${dir}/node_modules/@edx/paragon/dist/core.min.css`;
const pathToThemeVariantLightCss = `${dir}/node_modules/@edx/paragon/dist/light.min.css`;
Expand Down Expand Up @@ -48,7 +51,58 @@ function getParagonThemeCss(dir) {
};
}

/**
* TODO
*/
function getParagonCacheGroups(paragonThemeCss) {
const cacheGroups = {};
if (!paragonThemeCss) {
return cacheGroups;
}
const getCacheGroupName = (module, _, cacheGroupKey) => {
const buildHash = module.buildInfo.hash;
const moduleFileName = module
.identifier()
.split('/')
.reduceRight((item) => item)
.split('|')[0]
.split('.css')[0];
const outputChunkFilename = `${cacheGroupKey}.${buildHash}.${moduleFileName}`;
return outputChunkFilename;
};

cacheGroups[paragonThemeCss.core.entryName] = {
type: 'css/mini-extract',
name: getCacheGroupName,
chunks: chunk => chunk.name === paragonThemeCss.core.entryName,
enforce: true,
};
cacheGroups[paragonThemeCss.variants.light.entryName] = {
type: 'css/mini-extract',
name: getCacheGroupName,
chunks: chunk => chunk.name === paragonThemeCss.variants.light.entryName,
enforce: true,
};

return cacheGroups;
}

function getParagonEntryPoints(paragonThemeCss) {
const entryPoints = {};
if (!paragonThemeCss) {
return entryPoints;
}
entryPoints[paragonThemeCss.core.entryName] = path.resolve(process.cwd(), paragonThemeCss.core.filePath);
entryPoints[paragonThemeCss.variants.light.entryName] = path.resolve(
process.cwd(),
paragonThemeCss.variants.light.filePath,
);
return entryPoints;
}

module.exports = {
getParagonVersion,
getParagonThemeCss,
getParagonCacheGroups,
getParagonEntryPoints,
};
43 changes: 6 additions & 37 deletions config/webpack.common.config.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,19 @@
const path = require('path');
const webpack = require('webpack');
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');

const ParagonWebpackPlugin = require('../lib/plugins/paragon-webpack-plugin/ParagonWebpackPlugin');
const {
getParagonVersion,
getParagonThemeCss,
getParagonCacheGroups,
getParagonEntryPoints,
} = require('./data/paragonUtils');

const paragonThemeCss = getParagonThemeCss(process.cwd());
const paragonEntryPoints = {};
const paragonCacheGroups = {};
if (paragonThemeCss) {
paragonEntryPoints[paragonThemeCss.core.entryName] = path.resolve(process.cwd(), paragonThemeCss.core.filePath);
paragonEntryPoints[paragonThemeCss.variants.light.entryName] = path.resolve(
process.cwd(),
paragonThemeCss.variants.light.filePath,
);

paragonCacheGroups[paragonThemeCss.core.entryName] = {
type: 'css/mini-extract',
name: paragonThemeCss.core.outputChunkName,
chunks: chunk => chunk.name === paragonThemeCss.core.entryName,
enforce: true,
};
paragonCacheGroups[paragonThemeCss.variants.light.entryName] = {
type: 'css/mini-extract',
name: paragonThemeCss.variants.light.outputChunkName,
chunks: chunk => chunk.name === paragonThemeCss.variants.light.entryName,
enforce: true,
};
}

module.exports = {
entry: {
app: path.resolve(process.cwd(), './src/index'),
...paragonEntryPoints,
...getParagonEntryPoints(paragonThemeCss),
},
output: {
path: path.resolve(process.cwd(), './dist'),
Expand All @@ -55,22 +34,12 @@ module.exports = {
splitChunks: {
chunks: 'all',
cacheGroups: {
...paragonCacheGroups,
...getParagonCacheGroups(paragonThemeCss),
},
},
},
plugins: [
new RemoveEmptyScriptsPlugin(),
new webpack.DefinePlugin({
PARAGON: JSON.stringify({
version: getParagonVersion(process.cwd()),
themeUrls: {
core: paragonThemeCss.core,
variants: {
light: paragonThemeCss.variants.light,
},
},
}),
}),
new ParagonWebpackPlugin(),
],
};
4 changes: 1 addition & 3 deletions config/webpack.dev.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ function getStyleUseConfig({ isMinified = false } = {}) {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
...postCssPlugins,
],
plugins: postCssPlugins,
},
},
},
Expand Down
43 changes: 20 additions & 23 deletions example/src/ParagonPreview.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import React from 'react';

const ParagonPreview = () => {
if (!PARAGON) {
return null;
}
return (
<>
<h2>Paragon</h2>
<ul>
<li>
<a href={`/${PARAGON.themeUrls.core.outputChunkName}.css`} target="_blank" rel="noopener noreferrer">
{`${PARAGON.themeUrls.core.outputChunkName}.css`}
</a>
</li>
<li>
<a href={`/${PARAGON.themeUrls.variants.light.outputChunkName}.css`} target="_blank" rel="noopener noreferrer">
{`${PARAGON.themeUrls.variants.light.outputChunkName}.css`}
</a>
</li>
</ul>
<pre>{JSON.stringify(PARAGON, null, 2)}</pre>
</>
);
};
const ParagonPreview = () => (
<>
<h2>Paragon</h2>
<h3>Exposed Theme CSS</h3>
<ul>
<li>
<a href={`/${PARAGON.themeUrls.core}`} target="_blank" rel="noopener noreferrer">
{PARAGON.themeUrls.core}
</a>
</li>
<li>
<a href={`/${PARAGON.themeUrls.variants.light}`} target="_blank" rel="noopener noreferrer">
{PARAGON.themeUrls.variants.light}
</a>
</li>
</ul>
<h3>Contents of <code>PARAGON</code> global variable</h3>
<pre>{JSON.stringify(PARAGON, null, 2)}</pre>
</>
);

export default ParagonPreview;
123 changes: 123 additions & 0 deletions lib/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* eslint-disable no-underscore-dangle */
const { Compilation, sources } = require('webpack');
const parse5 = require('parse5');
const {
getParagonVersion,
getParagonThemeCss,
} = require('../../../config/data/paragonUtils');

const paragonVersion = getParagonVersion(process.cwd());
const paragonThemeCss = getParagonThemeCss(process.cwd());

class ParagonWebpackPlugin {
constructor() {
this.version = paragonVersion;
if (paragonThemeCss) {
this.coreEntryName = paragonThemeCss.core.entryName;
this.themeVariantEntryNames = {};
Object.entries(paragonThemeCss.variants).forEach(([key, value]) => {
this.themeVariantEntryNames[key] = value.entryName;
});
}
}

logger(message) {
console.log('[ParagonWebpackPlugin]', message);
}

getDescendantByTag(node, tag) {
for (let i = 0; i < node.childNodes?.length; i++) {
if (node.childNodes[i].tagName === tag) {
return node.childNodes[i];
}
const result = this.getDescendantByTag(node.childNodes[i], tag);
if (result) {
return result;
}
}
return null;
}

apply(compiler) {
compiler.hooks.thisCompilation.tap('ParagonWebpackPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'ParagonWebpackPlugin',
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
additionalAssets: true,
},
() => {
const file = compilation.getAsset('index.html');
if (!file) {
return;
}

// get paragon assets
const paragonAssets = compilation.getAssets().filter((asset) => asset.name.includes('paragon') && asset.name.endsWith('.css'));
const coreCssAsset = paragonAssets.find((asset) => asset.name.includes(this.coreEntryName))?.name;
const themeVariantLightAsset = paragonAssets.find((asset) => (
asset.name.includes(this.themeVariantEntryNames.light)
))?.name;
if (!coreCssAsset || !themeVariantLightAsset) {
this.logger('Unable to find paragon assets in compilation. Skipping.');
return;
}

const originalSource = file.source.source();
const newSource = new sources.ReplaceSource(
new sources.RawSource(originalSource),
'index.html',
);

// parse file as html document
const document = parse5.parse(originalSource, {
sourceCodeLocationInfo: true,
});

// find the body element
const bodyElement = this.getDescendantByTag(document, 'body');
if (!bodyElement) {
throw new Error('Missing body element in index.html');
}

// determine script insertion point
let scriptInsertionPoint;
if (bodyElement.sourceCodeLocation?.endTag) {
scriptInsertionPoint = bodyElement.sourceCodeLocation.endTag.startOffset;
} else {
// less accurate fallback
scriptInsertionPoint = originalSource.indexOf('</body>');
}

// update index.html with new content
const paragonScript = `
<script type="text/javascript">
var PARAGON = {
version: '${this.version}',
themeUrls: {
core: '${coreCssAsset}',
variants: {
light: '${themeVariantLightAsset}',
},
},
};
</script>
`
// minify the above script
.replace(/>[\r\n ]+</g, '><')
.replace(/(<.*?>)|\s+/g, (m, $1) => {
if ($1) { return $1; }
return ' ';
})
.trim();

// insert the Paragon script into the HTML document
newSource.insert(scriptInsertionPoint, paragonScript);
compilation.updateAsset('index.html', new sources.RawSource(newSource.source()));
},
);
});
}
}

module.exports = ParagonWebpackPlugin;
3 changes: 3 additions & 0 deletions lib/plugins/paragon-webpack-plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const ParagonWebpackPlugin = require('./ParagonWebpackPlugin');

module.exports = ParagonWebpackPlugin;
18 changes: 15 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"image-minimizer-webpack-plugin": "3.8.2",
"jest": "26.6.3",
"mini-css-extract-plugin": "1.6.2",
"parse5": "7.1.2",
"postcss": "8.4.24",
"postcss-custom-media": "^9.1.2",
"postcss-loader": "7.3.2",
Expand Down

0 comments on commit 4745a57

Please sign in to comment.