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

Enhancement/issue 763 custom css minification and bundling #980

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
db3b9f0
Enhancement/issue 971 refactor bundling and optimizations (#974)
thescientist13 Sep 22, 2022
4370ff5
v0.27.0-alpha.0
thescientist13 Oct 1, 2022
03713bd
(WIP) swap PostCSS in CLI with custom AST solution
thescientist13 Oct 1, 2022
25492bb
refactor website CSS
thescientist13 Oct 1, 2022
2b13887
basic implementation of minified CSS from AST
thescientist13 Oct 6, 2022
8876502
support relative @import statements
thescientist13 Oct 7, 2022
7d1f5ca
refactor AST parsing to recursive function
thescientist13 Oct 7, 2022
85d9d82
support deeply nested @import and CSS custom properties
thescientist13 Oct 7, 2022
e9c51d1
fix missing declaration semicolon
thescientist13 Oct 7, 2022
6b73c01
correctly close Rule brackets
thescientist13 Oct 8, 2022
ff356b2
general on leave refactoring
thescientist13 Oct 8, 2022
8e7639e
more selector support
thescientist13 Oct 8, 2022
a143fd3
all specs passing
thescientist13 Oct 8, 2022
081ba52
support percentage
thescientist13 Oct 14, 2022
08c6c8e
test for percentage
thescientist13 Oct 14, 2022
add9751
support url and @import url
thescientist13 Oct 14, 2022
8484c15
add important support
thescientist13 Oct 19, 2022
5cd3a53
custom implementation for handling matchers in attribute selectors
thescientist13 Oct 20, 2022
a176ef6
restore website prism styles
thescientist13 Oct 22, 2022
62fe773
nth and lang selectors support
thescientist13 Oct 22, 2022
c758342
improve support for matching selector types
thescientist13 Nov 11, 2022
0e54ed9
add error logging for CSS parsing
thescientist13 Nov 11, 2022
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
4 changes: 1 addition & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,14 @@
"acorn": "^8.0.1",
"acorn-walk": "^8.0.0",
"commander": "^2.20.0",
"cssnano": "^5.0.11",
"css-tree": "^2.2.1",
"es-module-shims": "^1.2.0",
"front-matter": "^4.0.2",
"koa": "^2.13.0",
"livereload": "^0.9.1",
"markdown-toc": "^1.2.0",
"node-fetch": "^2.6.1",
"node-html-parser": "^1.2.21",
"postcss": "^8.3.11",
"postcss-import": "^13.0.0",
"rehype-raw": "^5.0.0",
"rehype-stringify": "^8.0.0",
"remark-frontmatter": "^2.0.0",
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ async function bundleStyleResources(compilation, optimizationPlugins) {
const url = resource.sourcePathURL.pathname;
let optimizedStyles = await fs.promises.readFile(url, 'utf-8');

for (const plugin of optimizationPlugins) {
optimizedStyles = await plugin.shouldIntercept(url, optimizedStyles)
? (await plugin.intercept(url, optimizedStyles)).body
: optimizedStyles;
}

for (const plugin of optimizationPlugins) {
optimizedStyles = await plugin.shouldOptimize(url, optimizedStyles)
? await plugin.optimize(url, optimizedStyles)
Expand Down Expand Up @@ -127,7 +133,8 @@ const bundleCompilation = async (compilation) => {
}).map((plugin) => {
return plugin.provider(compilation);
}).filter((provider) => {
return provider.shouldOptimize && provider.optimize;
return provider.shouldIntercept && provider.intercept
|| provider.shouldOptimize && provider.optimize;
});
// centrally register all static resources
compilation.graph.map((page) => {
Expand Down
194 changes: 182 additions & 12 deletions packages/cli/src/plugins/resource/plugin-standard-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,189 @@
*
*/
import fs from 'fs';
import { parse, walk } from 'css-tree';
import path from 'path';
import cssnano from 'cssnano';
import postcss from 'postcss';
import postcssImport from 'postcss-import';
import { ResourceInterface } from '../../lib/resource-interface.js';

function bundleCss(body, url) {
const ast = parse(body, {
onParseError(error) {
console.log(error.formattedMessage);
}
});
let optimizedCss = '';

walk(ast, {
enter: function (node, item) { // eslint-disable-line complexity
const { type, name, value } = node;

if ((type === 'String' || type === 'Url') && this.atrulePrelude && this.atrule.name === 'import') {
const { value } = node;

if (value.indexOf('.') === 0) {
const importContents = fs.readFileSync(path.resolve(path.dirname(url), value), 'utf-8');

optimizedCss += bundleCss(importContents, url);
}
} else if (type === 'Atrule' && name !== 'import') {
optimizedCss += `@${name} `;
} else if (type === 'TypeSelector') {
optimizedCss += name;
} else if (type === 'IdSelector') {
optimizedCss += `#${name}`;
} else if (type === 'ClassSelector') {
optimizedCss += `.${name}`;
} else if (type === 'PseudoClassSelector') {
optimizedCss += `:${name}`;

switch (name) {

case 'lang':
case 'not':
case 'nth-child':
case 'nth-last-child':
case 'nth-of-type':
case 'nth-last-of-type':
optimizedCss += '(';
break;
default:
break;

}
} else if (type === 'Function') {
optimizedCss += `${name}(`;
} else if (type === 'MediaFeature') {
optimizedCss += ` (${name}:`;
} else if (type === 'PseudoElementSelector') {
optimizedCss += `::${name}`;
} else if (type === 'Block') {
optimizedCss += '{';
} else if (type === 'AttributeSelector') {
optimizedCss += '[';
} else if (type === 'Combinator') {
optimizedCss += name;
} else if (type === 'Nth') {
const { nth } = node;

switch (nth.type) {

case 'AnPlusB':
if (nth.a) {
optimizedCss += nth.a === '-1' ? '-n' : `${nth.a}n`;
}
if (nth.b) {
optimizedCss += nth.a ? `+${nth.b}` : nth.b;
}
break;
default:
break;

}
} else if (type === 'Declaration') {
optimizedCss += `${node.property}:`;
} else if (type === 'Url' && this.atrule?.name !== 'import') {
optimizedCss += `url('${node.value}')`;
} else if (type === 'Identifier' || type === 'Hash' || type === 'Dimension' || type === 'Number' || (type === 'String' && (this.atrule?.type !== 'import')) || type === 'Operator' || type === 'Raw' || type === 'Percentage') { // eslint-disable-line max-len
if (item && item.prev && type !== 'Operator' && item.prev.data.type !== 'Operator') {
optimizedCss += ' ';
}

switch (type) {

case 'Dimension':
optimizedCss += `${value}${node.unit}`;
break;
case 'Percentage':
optimizedCss += `${value}%`;
break;
case 'Hash':
optimizedCss += `#${value}`;
break;
case 'Identifier':
optimizedCss += name;
break;
case 'Number':
optimizedCss += value;
break;
case 'Operator':
optimizedCss += value;
break;
case 'String':
optimizedCss += `'${value}'`;
break;
case 'Raw':
optimizedCss += `${value.trim()}`;
break;
default:
break;

}
}
},
leave: function(node, item) {
switch (node.type) {

case 'Atrule':
if (node.name !== 'import') {
optimizedCss += '}';
}
break;
case 'Rule':
optimizedCss += '}';
break;
case 'Function':
case 'MediaFeature':
optimizedCss += ')';
break;
case 'PseudoClassSelector':
switch (node.name) {

case 'lang':
case 'not':
case 'nth-child':
case 'nth-last-child':
case 'nth-last-of-type':
case 'nth-of-type':
optimizedCss += ')';
break;
default:
break;

}
break;
case 'Declaration':
if (node.important) {
optimizedCss += '!important';
}

optimizedCss += ';';
break;
case 'Selector':
if (item.next) {
optimizedCss += ',';
}
break;
case 'AttributeSelector':
if (node.matcher) {
// TODO better way to do this?
// https://github.com/csstree/csstree/issues/207
const name = node.name.name;
const value = node.value.type === 'Identifier' ? node.value.name : `'${node.value.value}'`;

optimizedCss = optimizedCss.replace(`${name}${value}`, `${name}${node.matcher}${value}`);
}
optimizedCss += ']';
break;
default:
break;

}
}
});

return optimizedCss;
}

class StandardCssResource extends ResourceInterface {
constructor(compilation, options) {
super(compilation, options);
Expand Down Expand Up @@ -41,15 +218,8 @@ class StandardCssResource extends ResourceInterface {

async optimize(url, body) {
return new Promise(async (resolve, reject) => {
try {
const { outputDir, userWorkspace } = this.compilation.context;
const workspaceUrl = url.replace(outputDir, userWorkspace);
const contents = body || await fs.promises.readFile(url, 'utf-8');
const css = (await postcss([cssnano])
.use(postcssImport())
.process(contents, { from: workspaceUrl })).css;

resolve(css);
try {
resolve(bundleCss(body, url));
} catch (e) {
reject(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* theme.css
*/
import chai from 'chai';
import fs from 'fs';
import glob from 'glob-promise';
import { JSDOM } from 'jsdom';
import path from 'path';
Expand All @@ -33,6 +34,7 @@ describe('Build Greenwood With: ', function() {
const LABEL = 'Default Optimization Configuration';
const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
const outputPath = fileURLToPath(new URL('.', import.meta.url));
const expectedCss = fs.readFileSync(path.join(outputPath, './fixtures/expected.css'), 'utf-8').replace(/\n/g, '');
let runner;

before(function() {
Expand Down Expand Up @@ -79,7 +81,7 @@ describe('Build Greenwood With: ', function() {

describe('<link> tag and preloading', function() {
it('should contain one style.css in the output directory', async function() {
expect(await glob.promise(`${path.join(this.context.publicDir, 'styles')}/theme.*.css`)).to.have.lengthOf(1);
expect(await glob.promise(`${path.join(this.context.publicDir, 'styles')}/main.*.css`)).to.have.lengthOf(1);
});

it('should have the expected <link> tag in the <head>', function() {
Expand All @@ -96,9 +98,18 @@ describe('Build Greenwood With: ', function() {
.filter(link => link.getAttribute('as') === 'style');

expect(preloadLinkTags.length).to.be.equal(1);
expect(preloadLinkTags[0].href).to.match(/\/styles\/theme.*.css/);
expect(preloadLinkTags[0].href).to.match(/\/styles\/main.*.css/);
expect(preloadLinkTags[0].getAttribute('crossorigin')).to.equal('anonymous');
});

// test custom CSS bundling
it('should have the expect preload CSS content in the file', async function() {
const cssFiles = await glob.promise(path.join(this.context.publicDir, 'styles/*.css'));
const customCss = await fs.promises.readFile(cssFiles[0], 'utf-8');

expect(cssFiles.length).to.be.equal(1);
expect(customCss).to.be.equal(expectedCss);
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
:root,:host{--primary-color:#16f;--secondary-color:#ff7;}

@font-face {font-family:'Source Sans Pro';font-style:normal;font-weight:400;font-display:swap;src:local('Source Sans Pro Regular'),local('SourceSansPro-Regular'),url('/assets/fonts/source-sans-pro-v13-latin-regular.woff2')format('woff2'),url('/assets/fonts/source-sans-pro-v13-latin-regular.woff')format('woff'),url('/assets/fonts/source-sans-pro-v13-latin-regular.ttf')format('truetype');}

*{margin:0;padding:0;font-family:'Comic Sans',sans-serif;}

body{background-color:green;}

h1,h2{color:var(--primary-color);border:0.5px solid #dddde1;}

#foo,.bar{color:var(--secondary-color);}

div>p{display:none;}

a[title]{color:purple;}

@media screen and (max-width:992px){body{background-color:blue;}}

p::first-line{color:blue;width:100%!important;}

pre[class*='language-']{color:#ccc;background:none;}

dd:only-of-type{background-color:bisque;}

:not(pre)>code[class*='language-']{background:#2d2d2d;}

li:nth-child(-n+3){border:2px solid orange;margin-bottom:1px;}

li:nth-child(even){background-color:lightyellow;}

li:nth-last-child(5n){border:2px solid orange;margin-top:1px;}

dd:nth-last-of-type(odd){border:2px solid orange;}

p:nth-of-type(2n+1){color:red;}

*:lang(en-US){outline:2px solid deeppink;}

p~ul{font-weight:bold;}

a[href*='greenwood'],a[href$='.pdf']{color:orange;}

[title~=flower],a[href^='https'],[lang|=en]{text-decoration:underline;}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<head>
<script type="module" src="/components/header.js"></script>
<link rel="stylesheet" href="/styles/theme.css"></link>
<link rel="stylesheet" href="/styles/main.css"></link>
</head>

<body>
Expand Down
Loading