Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit b9d771f

Browse files
initial spike
1 parent 84d50d8 commit b9d771f

File tree

17 files changed

+5402
-3
lines changed

17 files changed

+5402
-3
lines changed

‎index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
/* eslint-env node */
22
'use strict';
3+
const processTrees = require('./lib/process-trees');
34

45
module.exports = {
5-
name: 'optimizing-css-compiler'
6+
name: 'optimizing-css-compiler',
7+
8+
included: function() {
9+
this.app.trees.app = processTrees(this.app.trees.app);
10+
}
611
};

‎lib/broccoli-plugins/optimize-css.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* jshint node: true */
2+
/**
3+
* OptimizingCssCompiler
4+
*
5+
* - takes a tree of glimmer templates and styles
6+
* - constructs a graph of all possible style paths
7+
* - constructs a new graph of all used style paths
8+
* - compiles css down to the smallest possible set of rules.
9+
* - applies those rules to the templates
10+
* - writes a style sheet containing the rules
11+
*/
12+
const Plugin = require('broccoli-plugin');
13+
const fs = require('fs');
14+
const path = require('path');
15+
const processStylesTree = require('../optimize/process-styles-tree');
16+
const processTemplatesTree = require('../optimize/process-templates-tree');
17+
const optimizeGraph = require('../optimize/optimize-styles-graph');
18+
const compileTemplatesTree = require('../optimize/compile-templates-tree');
19+
const writeStyleSheet = require('../optimize/write-style-sheet');
20+
21+
function OptimizingCssCompiler(inputPath, options) {
22+
options = options || {
23+
annotation: "Optimizing CSS Compiler"
24+
};
25+
this.options = options;
26+
27+
Plugin.call(this, [inputPath], {
28+
annotation: options.annotation
29+
});
30+
}
31+
32+
// Create a subclass from Plugin
33+
OptimizingCssCompiler.prototype = Object.create(Plugin.prototype);
34+
OptimizingCssCompiler.prototype.constructor = OptimizingCssCompiler;
35+
36+
OptimizingCssCompiler.prototype.build = function optimizeAndCompileCss() {
37+
const inputPath = this.inputPaths[0];
38+
const templatesPath = path.join(inputPath, 'templates');
39+
const stylesPath = path.join(inputPath, 'styles');
40+
const templatesDirStats = fs.lstatSync(templatesPath);
41+
const styleDirStats = fs.lstatSync(stylesPath);
42+
const outputPath = this.outputPath;
43+
44+
if (!templatesDirStats.isDirectory()) {
45+
throw new Error('Passed a tree with no /templates root directory to the Optimize CSS Compiler');
46+
}
47+
48+
if (!styleDirStats.isDirectory()) {
49+
throw new Error('Passed a tree with no /styles root directory to the Optimize CSS Compiler');
50+
}
51+
52+
return processStylesTree(stylesPath)
53+
.then((stylesGraph) => {
54+
return processTemplatesTree(templatesPath, stylesGraph);
55+
})
56+
.then(optimizeGraph)
57+
.then((optimizedStylesGraph) => {
58+
return compileTemplatesTree(templatesPath, optimizedStylesGraph, outputPath);
59+
})
60+
.then((optimizedStylesGraph) => {
61+
return writeStyleSheet(optimizedStylesGraph, outputPath);
62+
});
63+
};
64+
65+
module.exports = OptimizingCssCompiler;

‎lib/optimize/compile-templates-tree.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = function compileTemplatesTree(templatesPath, optimizedStylesGraph, outputPath) {
2+
Promise.resolve(optimizedStylesGraph);
3+
};

‎lib/optimize/optimize-styles-graph.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = function optimizeStylesGraph(stylesGraph) {
2+
return stylesGraph;
3+
};

‎lib/optimize/process-styles-tree.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const walkTree = require('./utils/walk-tree');
2+
const postcss = require('postcss');
3+
const cssGraphBuilder = require('../walkers/css-graph-builder');
4+
const COMPONENT_PATH = 'components/';
5+
6+
module.exports = function processStylesTree(styleDir /*, warningStream*/) {
7+
const GRAPH = {
8+
dynamic: Object.create(null),
9+
static: Object.create(null)
10+
};
11+
12+
return walkTree(styleDir, (relativePath, fileData) => {
13+
let moduleName = relativePath.substr(styleDir.length + 1);
14+
let isComponent = false;
15+
moduleName = moduleName.replace('.css', '');
16+
17+
if (moduleName.indexOf(COMPONENT_PATH) === 0) {
18+
isComponent = true;
19+
moduleName = moduleName.substr(COMPONENT_PATH.length);
20+
}
21+
22+
const processor = postcss();
23+
const opts = {
24+
from: relativePath,
25+
to: relativePath,
26+
map: {
27+
inline: false,
28+
annotation: false
29+
},
30+
plugins: [cssGraphBuilder],
31+
graph: GRAPH,
32+
isComponent,
33+
moduleName
34+
};
35+
36+
if (!opts.plugins || opts.plugins.length < 1) {
37+
throw new Error('You must provide at least 1 plugin in the plugin array')
38+
}
39+
40+
opts.plugins.forEach((plugin) => {
41+
// let pluginOptions = assign(opts, plugin.options || {});
42+
processor.use(plugin(opts));
43+
});
44+
45+
return processor.process(fileData, opts)
46+
.then((result) => {
47+
// result.warnings().forEach(warn => warningStream.write(warn.toString()))
48+
49+
return result.css
50+
})
51+
.catch((err) => {
52+
if (err.name === 'CssSyntaxError') {
53+
err.message += `\n${err.showSourceCode()}`
54+
}
55+
56+
throw err
57+
})
58+
}).then(() => {
59+
return GRAPH;
60+
});
61+
};

‎lib/optimize/process-templates-tree.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const walkTree = require('./utils/walk-tree');
2+
const precompile = require('@glimmer/compiler').precompile;
3+
const htmlGraphBuilder = require('../walkers/html-graph-builder');
4+
const COMPONENT_PATH = 'components/';
5+
6+
module.exports = function processTemplatesTree(templatesDir, stylesGraph) {
7+
const usedStylesGraph = {
8+
dynamic: Object.create(null),
9+
static: Object.create(null)
10+
};
11+
12+
return walkTree(templatesDir, (relativePath, fileData) => {
13+
let moduleName = relativePath.substr(templatesDir.length + 1);
14+
let isComponent = false;
15+
moduleName = moduleName.replace('.hbs', '');
16+
17+
if (moduleName.indexOf(COMPONENT_PATH) === 0) {
18+
isComponent = true;
19+
moduleName = moduleName.substr(COMPONENT_PATH.length);
20+
}
21+
22+
precompile(fileData, {
23+
rawSource: fileData,
24+
isComponent,
25+
moduleName,
26+
availableGraph:stylesGraph,
27+
usedGraph:usedStylesGraph,
28+
plugins: {
29+
ast: [htmlGraphBuilder]
30+
}
31+
});
32+
}).then(() => {
33+
return usedStylesGraph;
34+
});
35+
};

‎lib/optimize/utils/walk-tree.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Walks a Directory recursively, calling the callback for each file found with the file's path and contents
3+
*/
4+
const fs = require('fs');
5+
const path = require('path');
6+
7+
function walkTree(dirPath, cb) {
8+
return new Promise((resolve, reject) => {
9+
fs.readdir(dirPath, { encoding: 'utf-8' }, (err, files) => {
10+
if (err) { reject(err); }
11+
else {
12+
processDir(dirPath, files, cb).then(resolve, reject);
13+
}
14+
});
15+
});
16+
};
17+
18+
function processDir(dirPath, files, cb) {
19+
let awaits = [];
20+
21+
for (let i = 0; i < files.length; i++) {
22+
let file = files[i];
23+
let fullPath = path.join(dirPath, file);
24+
if (fs.lstatSync(fullPath).isDirectory()) {
25+
awaits.push(walkTree(fullPath, cb));
26+
} else {
27+
awaits.push(processFile(fullPath, cb));
28+
}
29+
}
30+
31+
return Promise.all(awaits);
32+
}
33+
34+
function processFile(file, cb) {
35+
return new Promise((resolve, reject) => {
36+
fs.readFile(file, { encoding: 'utf-8' }, (err, data) => {
37+
if (err) { reject(err); }
38+
else {
39+
resolve(cb(file, data));
40+
}
41+
});
42+
});
43+
}
44+
45+
module.exports = walkTree;

‎lib/optimize/write-style-sheet.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = function writeStyleSheet(optimizedStylesGraph, outputPath) {
2+
Promise.resolve();
3+
};

‎lib/process-trees.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const Funnel = require('broccoli-funnel');
2+
const mergeTrees = require('broccoli-merge-trees');
3+
const OptimizingCssCompiler = require('./broccoli-plugins/optimize-css');
4+
5+
module.exports = function processTrees(path) {
6+
const stylesAndTemplatesTree = new Funnel(path, {
7+
include: [/styles/, /templates/]
8+
});
9+
const passThroughTree = new Funnel(path, {
10+
exclude: [/styles/, /templates/]
11+
});
12+
const optimized = new OptimizingCssCompiler(stylesAndTemplatesTree);
13+
14+
return mergeTrees([passThroughTree, optimized]);
15+
};

‎lib/walkers/css-graph-builder.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const postcss = require('postcss');
2+
const LookupIndex = Object.create(null);
3+
4+
const VALUE_SYMBOL = 'OPTIMIZING_CSS_COMPILER_STYLE_POD';
5+
const SELECTOR_SPLIT_SYMBOLS = ',';
6+
const PATH_SPLIT_SYMBOLS = ' ';
7+
const PATH_SPLIT_CLEANUP = /\s+/g;
8+
const SPLIT_SYMBOLS = /[\.|#]/;
9+
10+
function addSegmentToLookupIndex(key, tlds) {
11+
let cache = LookupIndex;
12+
tlds.forEach((s) => {
13+
cache[s] = cache[s] || Object.create(null);
14+
cache = cache[s];
15+
});
16+
cache[VALUE_SYMBOL] = key;
17+
}
18+
19+
function iterateSelectors(selector, cb) {
20+
let selectors = selector.split(SELECTOR_SPLIT_SYMBOLS);
21+
selectors.forEach((path) => {
22+
let segments = path
23+
.split('>')
24+
.join(' > ')
25+
.replace(PATH_SPLIT_CLEANUP, ' ')
26+
.split(PATH_SPLIT_SYMBOLS)
27+
.reverse();
28+
cb(segments);
29+
});
30+
}
31+
32+
function iteratePath(path, cb) {
33+
path.forEach((segment) => {
34+
let tlds = segment.split(SPLIT_SYMBOLS).sort().reverse();
35+
cb(segment, tlds, tlds.join(''));
36+
});
37+
}
38+
39+
function generateRuleGraph(node, root, componentName) {
40+
const stylePod = Object.create(null);
41+
let selector = node.selector;
42+
const isComponent = !!componentName;
43+
44+
if (!isComponent && selector === '&') {
45+
throw new Error('you forget the component name!');
46+
}
47+
48+
if (!node.nodes.length) { return; }
49+
node.nodes.forEach(decl => {
50+
stylePod[decl.prop] = decl.value;
51+
});
52+
53+
if (selector === '&') {
54+
root[componentName] = root[componentName] || Object.create(null);
55+
root[componentName][VALUE_SYMBOL] = stylePod;
56+
} else {
57+
iterateSelectors(selector, (path) => {
58+
let currentSegment = root;
59+
60+
if (isComponent) {
61+
path.push(componentName);
62+
}
63+
64+
iteratePath(path, (segment, tlds) => {
65+
let newSegment = currentSegment[segment] || Object.create(null);
66+
currentSegment[segment] = newSegment;
67+
currentSegment = newSegment;
68+
});
69+
70+
currentSegment[VALUE_SYMBOL] = stylePod;
71+
});
72+
}
73+
}
74+
75+
module.exports = postcss.plugin('postcss-build-graph', (options = {}) => {
76+
// Work with options here
77+
const moduleName = options.moduleName;
78+
const isComponent = options.isComponent;
79+
const GRAPH = options.graph;
80+
const StaticGraph = GRAPH.static;
81+
const DynamicGraph = GRAPH.dynamic;
82+
83+
return root => {
84+
let previousNode = null;
85+
for (let i = 0; i < root.nodes.length; i++) {
86+
let node = root.nodes[i];
87+
let type = node.type;
88+
if (previousNode && previousNode.type === 'comment' && previousNode.text === '@dynamic') {
89+
type = 'dynamic';
90+
} else if (type ==='atrule' && node.name === 'dynamic') {
91+
type = 'dynamic';
92+
node.selector = node.params;
93+
}
94+
95+
switch (type) {
96+
case 'rule':
97+
//console.log('rule', node);
98+
generateRuleGraph(node, StaticGraph, isComponent ? moduleName: false);
99+
break;
100+
case 'dynamic':
101+
generateRuleGraph(node, DynamicGraph, isComponent ? moduleName: false);
102+
break;
103+
default:
104+
break;
105+
}
106+
previousNode = node;
107+
}
108+
};
109+
});

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /