Skip to content

Commit 9ce37b7

Browse files
Cleanup build process, multi-steps
1 parent 13fb19c commit 9ce37b7

11 files changed

+342
-405
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules
22
npm-debug.log
33
.DS_STORE
44
*.swp
5+
tmp

build.js

+23-7
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,27 @@
1111

1212
// The rest of the build process is async and requires some promises
1313
// var comp_build = Promise.resolve(true); // uncomment and comment next to skip build
14-
var comp_build = require('./src/components-build')();
15-
comp_build.then(function () {
16-
console.log('components build complete')
17-
}).then(function () {
18-
return require('./src/components-screenshot-build')().then(function () {
19-
console.log('components screenshots complete')
20-
})
14+
15+
const componentsBuildIndex = require('./src/components-build-index');
16+
const componentsBuildPages = require('./src/components-build-pages');
17+
const componentsBuildScreenshots = require('./src/components-build-screenshots');
18+
19+
const globPattern = 'src/components/cards/*.html';
20+
componentsBuildIndex(globPattern)
21+
.then(componentsForNavPath => componentsBuildPages(componentsForNavPath))
22+
.then(componentsForNavPath => componentsBuildScreenshots(componentsForNavPath))
23+
.then(() => {
24+
console.log('All done');
2125
})
26+
.catch((err) => {
27+
console.log(err);
28+
});
29+
30+
// var comp_build = require('./src/components-build')();
31+
// comp_build.then(function () {
32+
// console.log('components build complete')
33+
// }).then(function () {
34+
// // return require('./src/components-screenshot-build')().then(function () {
35+
// // console.log('components screenshots complete')
36+
// // })
37+
// })

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"url": "https://github.com/tachyons-css/tachyons-css.github.io/issues"
3434
},
3535
"dependencies": {
36+
"chalk": "^1.1.3",
3637
"co": "^4.6.0",
3738
"express": "^4.14.0",
3839
"nightmare": "^2.8.1",

src/components-build-index.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const _ = require('lodash');
2+
const chalk = require('chalk');
3+
const fm = require('json-front-matter');
4+
const fs = require('fs');
5+
const glob = require('glob');
6+
const mkdirp = require('mkdirp');
7+
const path = require('path');
8+
const rmHtmlExt = require('remove-html-extension');
9+
const titleize = require('titleize');
10+
11+
const componentIndexPath = 'components/index.html';
12+
const componentsForNavPath = 'tmp/componentsForNav.json';
13+
const defaultComponentsGlobPattern = 'src/components/**/*.html';
14+
const screenshotBasename = 'screenshot';
15+
16+
const analyticsTemplatePath = 'src/templates/ga.html';
17+
const footerTemplatePath = 'src/templates/footer.html';
18+
const headerTemplatePath = 'src/templates/header.html';
19+
const headTemplatePath = 'src/templates/head.html';
20+
const indexTemplatePath = 'src/templates/components-index.html';
21+
22+
const getTitle = (component) => {
23+
const title = rmHtmlExt(component).replace('src/components/', '').replace(/(\/|_|-)/g, ' ');
24+
return titleize(title);
25+
};
26+
27+
const getName = component => titleize(getTitle(component.split('/')[3]));
28+
29+
module.exports = globPattern => new Promise((resolve, reject) => {
30+
glob(globPattern || defaultComponentsGlobPattern, {}, (err, components) => {
31+
console.log(chalk.magenta('Working on components index...'));
32+
if (err) {
33+
reject(err);
34+
}
35+
36+
console.log('- Found', components.length, 'components');
37+
38+
const componentsForNav = {};
39+
components.forEach((component) => {
40+
const componentTokens = component.replace('src/components/', '').split('/');
41+
const category = componentTokens[0];
42+
43+
// Check the front matter for screenshot overrides
44+
const componentHtml = fs.readFileSync(component, 'utf8');
45+
const fmParsed = fm.parse(componentHtml);
46+
const frontMatter = fmParsed.attributes || {};
47+
const dir = component.replace('src/', '').replace('.html', '');
48+
componentsForNav[category] = componentsForNav[category] || [];
49+
componentsForNav[category].push({
50+
name: frontMatter.name || getName(component),
51+
title: frontMatter.title || getTitle(component),
52+
src: component,
53+
path: `${dir}/index.html`,
54+
href: `/${dir}/index.html`,
55+
screenshot: {
56+
path: `${dir}/${screenshotBasename}.png`,
57+
href: `/${dir}/${screenshotBasename}.png`,
58+
},
59+
frontMatter,
60+
});
61+
});
62+
63+
mkdirp.sync(path.dirname(componentsForNavPath));
64+
fs.writeFileSync(componentsForNavPath, JSON.stringify(componentsForNav, undefined, 2));
65+
console.log('- Created navigation JSON:', componentsForNavPath);
66+
67+
const analytics = fs.readFileSync(analyticsTemplatePath, 'utf8');
68+
const footer = fs.readFileSync(footerTemplatePath, 'utf8');
69+
const head = fs.readFileSync(headTemplatePath, 'utf8');
70+
const header = fs.readFileSync(headerTemplatePath, 'utf8');
71+
const indexTemplate = fs.readFileSync(indexTemplatePath, 'utf8');
72+
73+
const compiledPage = _.template(indexTemplate)({
74+
componentsForNav,
75+
title: 'Components',
76+
analytics,
77+
footer,
78+
head,
79+
header,
80+
});
81+
mkdirp.sync(path.dirname(componentIndexPath));
82+
fs.writeFileSync(componentIndexPath, compiledPage);
83+
console.log('- Created index:', componentIndexPath);
84+
85+
console.log(chalk.magenta('Done with components index!'));
86+
resolve(componentsForNavPath);
87+
}); // glob
88+
}); // return promise

src/components-build-pages.js

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
const _ = require('lodash');
2+
const atImport = require('postcss-import');
3+
const chalk = require('chalk');
4+
const conditionals = require('postcss-conditionals');
5+
const cssstats = require('cssstats');
6+
const cssVariables = require('postcss-css-variables');
7+
const customMedia = require('postcss-custom-media');
8+
const escapeHtml = require('escape-html');
9+
const fm = require('json-front-matter');
10+
const fs = require('fs');
11+
const getClasses = require('get-classes-from-html');
12+
const mkdirp = require('mkdirp');
13+
const mqPacker = require('css-mqpacker');
14+
const path = require('path');
15+
const perfectionist = require('perfectionist');
16+
const postcss = require('postcss');
17+
const removeComments = require('postcss-discard-comments');
18+
const removeEmpty = require('postcss-discard-empty');
19+
const select = require('postcss-select');
20+
21+
const analyticsTemplatePath = 'src/templates/ga.html';
22+
const componentTemplatePath = 'src/templates/components.html';
23+
const footerTemplatePath = 'src/templates/footer.html';
24+
const headTemplatePath = 'src/templates/head.html';
25+
const highlightTemplatePath = 'src/templates/highlight.html';
26+
27+
const tachyonsCssPath = 'src/css/tachyons.css';
28+
29+
module.exports = componentsForNavPath => new Promise((resolve, reject) => {
30+
console.log(chalk.magenta('Working on components pages...'));
31+
if (componentsForNavPath === undefined || !fs.existsSync(componentsForNavPath)) {
32+
reject('Can not find components nav JSON file');
33+
}
34+
const componentsForNav = JSON.parse(fs.readFileSync(componentsForNavPath, 'utf8'));
35+
36+
const componentTemplate = fs.readFileSync(componentTemplatePath, 'utf8');
37+
const analytics = fs.readFileSync(analyticsTemplatePath, 'utf8');
38+
const footer = fs.readFileSync(footerTemplatePath, 'utf8');
39+
const head = fs.readFileSync(headTemplatePath, 'utf8');
40+
const highlight = fs.readFileSync(highlightTemplatePath, 'utf8');
41+
42+
const tachyonsCss = fs.readFileSync(tachyonsCssPath, 'utf8');
43+
44+
const promises = [];
45+
Object.keys(componentsForNav).forEach((category) => {
46+
console.log(chalk.yellow('- Processing category:'), category);
47+
componentsForNav[category].forEach((component) => {
48+
const componentHtml = fs.readFileSync(component.src, 'utf8');
49+
const fmParsed = fm.parse(componentHtml);
50+
51+
const frontMatter = _.assign({}, component.frontMatter);
52+
frontMatter.title = component.title;
53+
frontMatter.name = component.name;
54+
frontMatter.bodyClass = frontMatter.bodyClass || '';
55+
frontMatter.classes = getClasses(fmParsed.body).map(klass => `.${klass}`);
56+
frontMatter.componentHtml = componentHtml;
57+
frontMatter.content = fmParsed.body;
58+
frontMatter.escapedHtml = escapeHtml(fmParsed.body);
59+
frontMatter.footer = footer;
60+
frontMatter.analytics = analytics;
61+
frontMatter.head = head;
62+
frontMatter.highlight = highlight;
63+
frontMatter.componentsForNav = componentsForNav;
64+
65+
const moduleSrcs = {};
66+
const getModules = postcss.plugin('get-modules', () => (css) => {
67+
css.walkRules((rule) => {
68+
moduleSrcs[rule.source.input.from] = true;
69+
});
70+
});
71+
72+
const promise = postcss([
73+
atImport(),
74+
cssVariables(),
75+
conditionals(),
76+
customMedia(),
77+
select(frontMatter.classes),
78+
removeComments({ removeAll: true }),
79+
mqPacker(),
80+
removeEmpty(),
81+
getModules(),
82+
perfectionist(),
83+
]).process(tachyonsCss, {
84+
from: tachyonsCssPath,
85+
}).then((result) => {
86+
frontMatter.componentCss = result.css;
87+
frontMatter.stats = cssstats(frontMatter.componentCss);
88+
89+
// TODO: Update me once src/ uses the npm modules
90+
frontMatter.modules = Object.keys(moduleSrcs).map(
91+
module => `tachyons-${module.split('/_')[1].replace('.css', '')}`
92+
);
93+
94+
const compiledComponent = _.template(componentTemplate)(frontMatter);
95+
mkdirp.sync(path.dirname(component.path));
96+
fs.writeFileSync(component.path, compiledComponent);
97+
console.log(' * Created component:', component.path);
98+
}).catch((e) => {
99+
console.log(e);
100+
});
101+
promises.push(promise);
102+
});
103+
});
104+
resolve(Promise.all(promises).then(() => {
105+
console.log(chalk.magenta('Done with components pages!'));
106+
return componentsForNavPath;
107+
}));
108+
}); // return promise

src/components-build-screenshots.js

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
const chalk = require('chalk');
2+
const co = require('co');
3+
const express = require('express');
4+
const fs = require('fs');
5+
const Nightmare = require('nightmare');
6+
const path = require('path');
7+
8+
const screenshotTargetWidth = 1024;
9+
const screenshotMaxHeight = 768;
10+
const screenshotSelector = '[data-name="component-container"]';
11+
const serverPort = 3333;
12+
13+
module.exports = componentsForNavPath => new Promise((resolve, reject) => {
14+
console.log(chalk.magenta('Working on components screenshots...'));
15+
if (componentsForNavPath === undefined || !fs.existsSync(componentsForNavPath)) {
16+
reject('Can not find components nav JSON file');
17+
}
18+
const componentsForNav = JSON.parse(fs.readFileSync(componentsForNavPath, 'utf8'));
19+
20+
// Setup a quick static HTML server so that CSS is loaded correctly.
21+
const app = express();
22+
app.set('port', serverPort);
23+
app.use(express.static(path.join(__dirname, '..')));
24+
const server = app.listen(app.get('port'), () => {
25+
console.log('- Started static HTML server on port:', server.address().port);
26+
});
27+
28+
// Initialize nightmware now.
29+
const nightmare = Nightmare({ // eslint-disable-line
30+
show: false,
31+
frame: false,
32+
useContentSize: true,
33+
switches: {
34+
// Unfortunately .viewport() sets the viewport size in *CSS pixels*.
35+
// Let's force device pixel ratio to 1 otherwise screenshots will vary in size
36+
// depending on the machine running this script (double for Retina Macbook Pro)
37+
// https://github.com/segmentio/nightmare/issues/498#issuecomment-265948400
38+
'force-device-scale-factor': '1',
39+
},
40+
});
41+
42+
const renderPromise = co(function* generator() {
43+
const categories = Object.keys(componentsForNav);
44+
for (let cat_idx = 0; cat_idx < categories.length; cat_idx += 1) {
45+
const category = categories[cat_idx];
46+
console.log(chalk.yellow('- Processing category:'), category);
47+
48+
for (let comp_idx = 0; comp_idx < componentsForNav[category].length; comp_idx += 1) {
49+
const component = componentsForNav[category][comp_idx];
50+
let selector = screenshotSelector;
51+
if (component.frontMatter.screenshot && component.frontMatter.screenshot.selector) {
52+
selector = `${selector} ${component.frontMatter.screenshot.selector}`;
53+
}
54+
55+
// Grab the size of the component enclosing rectangle
56+
// https://github.com/segmentio/nightmare/issues/498#issuecomment-189156529
57+
const componentRect = yield nightmare
58+
.viewport(screenshotTargetWidth, screenshotMaxHeight)
59+
// .wait(1000)
60+
.goto(`http://localhost:${app.get('port')}${component.href}`)
61+
.wait(selector)
62+
.evaluate((_selector) => {
63+
// Hide scrollbar that could pop up due to .scrollTo
64+
const sheet = document.styleSheets[0];
65+
sheet.insertRule('::-webkit-scrollbar { display:none; }');
66+
const element = document.querySelector(_selector);
67+
if (element) {
68+
const rect = element.getBoundingClientRect();
69+
return {
70+
x: Math.round(rect.left),
71+
y: Math.round(rect.top),
72+
width: Math.round(rect.width),
73+
height: Math.round(rect.height),
74+
};
75+
}
76+
return false;
77+
}, selector);
78+
79+
// Capture the component
80+
if (componentRect !== false) {
81+
componentRect.height = Math.min(componentRect.height, screenshotMaxHeight);
82+
yield nightmare
83+
// we can not use .screenshot() with componentRect, so constrain the viewport instead
84+
.viewport(componentRect.width, componentRect.height)
85+
.scrollTo(componentRect.y, componentRect.x)
86+
// .wait(1000)
87+
// do *not* use componentRect in .screenshot() below or risk distortions
88+
.screenshot(component.screenshot.path)
89+
.then(() => {
90+
console.log(' * Created screenshot: ', component.screenshot.path);
91+
});
92+
}
93+
} // Loop over components
94+
} // Loop over categories
95+
yield nightmare.end();
96+
}).then(() => {
97+
console.log('- Closed static HTML server');
98+
server.close();
99+
console.log(chalk.magenta('Done with components screenshots!'));
100+
}, (err) => {
101+
console.error(err);
102+
reject(err);
103+
});
104+
105+
return resolve(renderPromise);
106+
107+
// TODO: optimize all PNG files, eventually resize to smaller width
108+
// const imagemin = require('imagemin');
109+
// const imageminPngquant = require('imagemin-pngquant');
110+
// imagemin(['components/*.{jpg,png}'], ??, {
111+
// plugins: [
112+
// imageminPngquant({quality: '65-80'})
113+
// ]
114+
// }).then(files => {
115+
// console.log(files);
116+
// //=> [{data: <Buffer 89 50 4e …>, path: 'build/images/foo.jpg'}, …]
117+
// });
118+
//
119+
}); // return promise

0 commit comments

Comments
 (0)