Skip to content

Commit

Permalink
Enhancement/issue 763 custom css minification and bundling (#980)
Browse files Browse the repository at this point in the history
* Enhancement/issue 971 refactor bundling and optimizations (#974)

* add cloud IDE caveat to puppeteer renderer plugin readme (#967)

* init commit of refactoring for script tags with a src

* initial CSS optimizing

* sync optimized link tags in final output

* refactored for shared reources

* handle inline script bundling

* support serving custom resources using Greenwood plugins in Rollup configuration without needing extra rollup plugin

* non resource related Rollup plugins supported

* custom resource plugins and rollup plugins working together

* handle empty input for Rollup

* updated lock file

* handle inline style tag bundling and optimizing

* default optimization spec passing

* refactor merging app and page templates

* clarifying corrections in spec files

* inline optimization config working

* none optimization support

* none optimization support

* none and static optimization overrides

* refactor html rendering and optimizing

* refactoring and more CLI specs passing

* add missing variable

* SSR specs and optimizing resource bundling

* minor refactoring and logging

* resolving some plugin specs

* restore develop command related GraphQL specs

* custom graphql query spec

* all specs passing

* drop rollup plugin deps from import typescript plugin

* all Greenwood commands and specs passing

* restore static router with custom prerender

* restore postcss-import

* refactor shared resources to a Map and handle dupes

* restore local packages workaround for local Rollup bundling

* better monorepo Rollup facade modules detection

* switch console log

* remove console logging

* update plugin related docs

* local solution for windows support

* refactor errant object assign

* full cross platform URL support

* fix lint

* fix extra bundles when custom prerendering

* clean up stale or already tracked TODOs

* add nested head tag smoke tests

* check for app template validation for HUD display

* misc refactoring and TODOs cleanup

* restore static router (again)

* standardize passing correct reference for prerender scripts

* clean up data-gwd-opt markers from final HTML

* v0.27.0-alpha.0

* (WIP) swap PostCSS in CLI with custom AST solution

* refactor website CSS

* basic implementation of minified CSS from AST

* support relative @import statements

* refactor AST parsing to recursive function

* support deeply nested @import and CSS custom properties

* fix  missing declaration semicolon

* correctly close Rule brackets

* general on leave refactoring

* more selector support

* all specs passing

* support percentage

* test for percentage

* support url and @import url

* add important support

* custom implementation for handling matchers in attribute selectors

* restore website prism styles

* nth and lang selectors support

* improve support for matching selector types

* add error logging for CSS parsing
  • Loading branch information
thescientist13 authored Nov 12, 2022
1 parent ce58b72 commit 9e792ae
Show file tree
Hide file tree
Showing 21 changed files with 396 additions and 552 deletions.
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

0 comments on commit 9e792ae

Please sign in to comment.