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 427 css bundling rollup #438

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
5 changes: 3 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,21 @@
"acorn": "^8.0.1",
"acorn-walk": "^8.0.0",
"commander": "^2.20.0",
"cssnano": "^4.1.10",
"es-module-shims": "^0.5.2",
"front-matter": "^4.0.2",
"htmlparser2": "^4.1.0",
"koa": "^2.13.0",
"livereload": "^0.9.1",
"markdown-toc": "^1.2.0",
"node-html-parser": "^1.2.21",
"postcss-import": "^12.0.0",
"puppeteer": "^5.3.0",
"rehype-stringify": "^8.0.0",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.3",
"remark-rehype": "^7.0.0",
"rollup": "^2.26.5",
"rollup-plugin-ignore-import": "^1.3.2",
"rollup": "^2.34.1",
"rollup-plugin-multi-input": "^1.1.1",
"rollup-plugin-postcss": "^3.1.5",
"rollup-plugin-terser": "^7.0.0",
Expand Down
8 changes: 3 additions & 5 deletions packages/cli/src/config/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
module.exports = {
// plugins: {
// 'postcss-preset-env': {}, // stage 2+
// 'postcss-nested': {},
// 'cssnano': {}
// }
plugins: [
require('cssnano')
]
};
144 changes: 105 additions & 39 deletions packages/cli/src/config/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
const crypto = require('crypto');
const fs = require('fs');
const fsPromises = require('fs').promises;
// TODO use node-html-parser
const htmlparser2 = require('htmlparser2');
const json = require('@rollup/plugin-json');
const multiInput = require('rollup-plugin-multi-input').default;
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const path = require('path');
const postcss = require('rollup-plugin-postcss');
const postcss = require('postcss');
const postcssConfig = require('./postcss.config');
const postcssImport = require('postcss-import');
const postcssRollup = require('rollup-plugin-postcss');
const { terser } = require('rollup-plugin-terser');

function greenwoodWorkspaceResolver (compilation) {
Expand All @@ -30,58 +33,125 @@ function greenwoodWorkspaceResolver (compilation) {

// https://github.com/rollup/rollup/issues/2873
function greenwoodHtmlPlugin(compilation) {
const { userWorkspace } = compilation.context;
const { userWorkspace, outputDir } = compilation.context;

return {
name: 'greenwood-html-plugin',
load(id) {
// console.debug('load id', id);
if (path.extname(id) === '.html') {
return '';
}
},
// TODO do this during load instead?
async buildStart(options) {
// TODO dont emit duplicate scripts, e.g. use a Map()
const mappedStyles = [];
const mappedScripts = new Map();
const that = this;
// TODO handle deeper paths. e.g. ../../../
const parser = new htmlparser2.Parser({
onopentag(name, attribs) {
if (name === 'script' && attribs.type === 'module' && attribs.src) {
// TODO handle deeper paths
const srcPath = attribs.src.replace('../', './');
const scriptSrc = fs.readFileSync(path.join(userWorkspace, srcPath), 'utf-8');

if (name === 'script' && attribs.type === 'module' && attribs.src && !mappedScripts.get(attribs.src)) {
const { src } = attribs;

// TODO avoid using src and set it to the value of rollup fileName
// since user paths can still be the same file, e.g. ../theme.css and ./theme.css are still the same file
mappedScripts.set(src, true);

const srcPath = src.replace('../', './');
const source = fs.readFileSync(path.join(userWorkspace, srcPath), 'utf-8');

that.emitFile({
type: 'chunk',
id: srcPath,
name: srcPath.split('/')[srcPath.split('/').length - 1].replace('.js', ''),
source: scriptSrc
source
});

// console.debug('emitFile for script => ', srcPath);
// console.debug('rollup emitFile (chunk)', srcPath);
}

if (name === 'link' && attribs.rel === 'stylesheet' && !mappedStyles[attribs.href]) {
// console.debug('found a stylesheet!', attribs);
let { href } = attribs;

if (href.charAt(0) === '/') {
href = href.slice(1);
}

// TODO handle auto expanding deeper paths
const filePath = path.join(userWorkspace, href);
const source = fs.readFileSync(filePath, 'utf-8');
const to = `${outputDir}/${href}`;
const hash = crypto.createHash('md5').update(source, 'utf8').digest('hex');
const fileName = href
.replace('.css', `.${hash.slice(0, 8)}.css`)
.replace('../', '')
.replace('./', '');

if (!fs.existsSync(path.dirname(to))) {
fs.mkdirSync(path.dirname(to), {
recursive: true
});
}

// TODO avoid using href and set it to the value of rollup fileName instead
// since user paths can still be the same file, e.g. ../theme.css and ./theme.css are still the same file
mappedStyles[attribs.href] = {
type: 'asset',
fileName,
name: href,
source
};

}
}
});

for (const input in options.input) {
const inputHtml = options.input[input];
const html = await fsPromises.readFile(inputHtml, 'utf-8');
const html = fs.readFileSync(inputHtml, 'utf-8');

parser.write(html);
parser.end();
parser.reset();
}

// this is a giant work around because PostCSS and some plugins can only be run async
// and so have to use with awit but _outside_ sync code, like parser / rollup
// https://github.com/cssnano/cssnano/issues/68
// https://github.com/postcss/postcss/issues/595
// TODO consider similar approach for emitting chunks?
return Promise.all(Object.keys(mappedStyles).map(async (assetKey) => {
const asset = mappedStyles[assetKey];
const filePath = path.join(userWorkspace, asset.name);

const result = await postcss(postcssConfig.plugins)
.use(postcssImport())
.process(asset.source, { from: filePath });

asset.source = result.css;

return new Promise((resolve, reject) => {
try {
that.emitFile(asset);
resolve();
} catch (e) {
reject(e);
}
});
}));
},
async generateBundle(outputOptions, bundles) {
generateBundle(outputOptions, bundles) {
const mappedBundles = new Map();

// console.debug('rollup generateBundle bundles', Object.keys(bundles));

// TODO looping over bundles twice is wildly inneficient, should refactor and safe references once
for (const bundleId of Object.keys(bundles)) {
const bundle = bundles[bundleId];

// TODO handle (!) Generated empty chunks .greenwood/about, .greenwood/index
if (bundle.isEntry && path.extname(bundle.facadeModuleId) === '.html') {
const html = await fsPromises.readFile(bundle.facadeModuleId, 'utf-8');
const html = fs.readFileSync(bundle.facadeModuleId, 'utf-8');
let newHtml = html;

const parser = new htmlparser2.Parser({
Expand All @@ -99,7 +169,7 @@ function greenwoodHtmlPlugin(compilation) {
} else {
// console.debug('NO MATCH?????', innerBundleId);
// TODO better testing
// TODO magic string
// TODO no magic strings
if (innerBundleId.indexOf('.greenwood/') < 0 && !mappedBundles.get(innerBundleId)) {
// console.debug('NEW BUNDLE TO INJECT!');
newHtml = newHtml.replace(/<script type="module" src="(.*)"><\/script>/, `
Expand All @@ -110,14 +180,25 @@ function greenwoodHtmlPlugin(compilation) {
}
}
}

if (name === 'link' && attribs.rel === 'stylesheet') {
for (const bundleId2 of Object.keys(bundles)) {
if (bundleId2.indexOf('.css') > 0) {
const bundle2 = bundles[bundleId2];
if (attribs.href.indexOf(bundle2.name) >= 0) {
newHtml = newHtml.replace(attribs.href, `/${bundle2.fileName}`);
}
}
}
}
}
});

parser.write(html);
parser.end();

// TODO this seems hacky; hardcoded dirs :D
bundle.fileName = bundle.facadeModuleId.replace('.greenwood', './public');
bundle.fileName = bundle.facadeModuleId.replace('.greenwood', 'public');
bundle.code = newHtml;
}
}
Expand All @@ -132,6 +213,7 @@ module.exports = getRollupConfig = async (compilation) => {
return [{
// TODO Avoid .greenwood/ directory, do everything in public/?
input: `${scratchDir}/**/*.html`,
// preserveEntrySignatures: false,
output: {
dir: outputDir,
entryFileNames: '[name].[hash].js',
Expand All @@ -145,34 +227,18 @@ module.exports = getRollupConfig = async (compilation) => {
}
},
plugins: [
// ignoreImport({
// include: ['**/*.css'],
// // extensions: ['.css']
// }),
nodeResolve(),
greenwoodWorkspaceResolver(compilation),
greenwoodHtmlPlugin(compilation),
multiInput(),
postcss({
postcssRollup({
extract: false,
minimize: true
minimize: true,
inject: false
}),
json(), // TODO bundle as part of import support?
json(), // TODO bundle as part of import support / transforms API?
terser()
]
}];

};

// }, {
// input: `${workspaceDirectory}/**/*.css`, // TODO emits a www/styles.js file?
// output: { // TODO CSS filename hashing / cache busting - https://github.com/egoist/rollup-plugin-postcss/pull/226
// dir: outputDirectory
// },
// plugins: [
// multiInput(),
// postcss({
// extract: true,
// minimize: true
// })
// ]
};
10 changes: 1 addition & 9 deletions packages/cli/src/lifecycles/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ let defaultConfig = {
port: 1984,
host: 'localhost'
},
// optimization: 'spa',
// TODO optimization: 'spa',
publicPath: '/',
title: 'My App',
meta: [],
plugins: [],
// TODO themeFile: 'theme.css',
markdown: { plugins: [], settings: {} }
};

Expand Down Expand Up @@ -101,13 +100,6 @@ module.exports = readAndMergeConfig = async() => {
// customConfig.plugins = customConfig.plugins.concat(plugins);
// }

// if (themeFile) {
// if (typeof themeFile !== 'string' && themeFile.indexOf('.') < 1) {
// reject(`Error: greenwood.config.js themeFile must be a valid filename. got ${themeFile} instead.`);
// }
// customConfig.themeFile = themeFile;
// }

if (devServer && Object.keys(devServer).length > 0) {

if (devServer.host) {
Expand Down
27 changes: 0 additions & 27 deletions packages/cli/src/lifecycles/copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,33 +69,6 @@ module.exports = copyAssets = (compilation) => {
console.info('copying graph.json...');
await copyFile(`${context.scratchDir}graph.json`, `${context.outputDir}/graph.json`);

// TODO should really be done by rollup
if (fs.existsSync(`${context.userWorkspace}`)) {
console.info('copying CSS files...');
const cssPaths = await rreaddir(context.userWorkspace);

if (cssPaths.length === 0) {
return;
}

await Promise.all(cssPaths.filter((cssPath) => {
if (path.extname(cssPath) === '.css') {
return cssPath;
}
}).map((cssPath) => {
const targetPath = cssPath.replace(context.userWorkspace, context.outputDir);
const targetDir = targetPath.replace(path.basename(targetPath), '');

if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, {
recursive: true
});
}

return copyFile(cssPath, targetPath);
}));
}

resolve();
} catch (err) {
reject(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('Build Greenwood With: ', function() {
let setup;

before(async function() {
setup = new TestBed(true);
setup = new TestBed();

this.context = await setup.setupTestBed(__dirname);
});
Expand Down
Loading