Skip to content

Commit ec0ed81

Browse files
committed
Merge pull request #21 from css-modules/enhancement
Enhancement
2 parents abc214f + c454228 commit ec0ed81

File tree

19 files changed

+470
-258
lines changed

19 files changed

+470
-258
lines changed

.eslintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
dist
22
coverage
33
node_modules
4+
5+
generate-tests.js
6+
test/common-test-cases.js

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
.git*
44
.npmignore
5+
generate-tests.js
56
test/
67
src/

generate-tests.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { existsSync, readdirSync, writeFile } from 'fs';
2+
import { join, resolve, sep } from 'path';
3+
import { toArray } from 'lodash';
4+
5+
const destination = resolve('test/common-test-cases.js');
6+
const cases = ['test-cases', 'cssi'];
7+
8+
function resolveTo() {
9+
const args = toArray(arguments);
10+
return resolve(join.apply(null, ['test'].concat(args)));
11+
}
12+
13+
let content =
14+
`import { equal } from 'assert';\n`+
15+
`import { readFileSync } from 'fs';\n`+
16+
`import { resolve } from 'path';\n`+
17+
`import { extend } from 'lodash';\n`+
18+
`import FileSystemLoader from 'css-modules-loader-core/lib/file-system-loader';\n`+
19+
`import hook from '../src';\n`+
20+
`\n`+
21+
`const normalize = str => str.replace(/\\r\\n?/g, '\\n');\n`+
22+
`const pipelines = {\n`+
23+
` 'test-cases': undefined,\n`+
24+
` 'cssi': [],\n`+
25+
`};\n`+
26+
`\n`+
27+
`let expectedCSS;\n`+
28+
`let expectedTokens;\n`+
29+
`\n`+
30+
`describe('common-test-cases', () => {\n`;
31+
32+
cases.forEach(dirname => {
33+
content +=
34+
`\n`+
35+
` describe('${dirname}', () => {\n`;
36+
37+
readdirSync(resolveTo(dirname)).forEach(testCase => {
38+
if (existsSync(resolveTo(dirname, testCase, 'source.css'))) {
39+
40+
content +=
41+
`\n`+
42+
` describe('${testCase.replace(/-/g, ' ')}', () => {\n`+
43+
` before(() => {\n`+
44+
` expectedCSS = normalize(readFileSync(resolve('test${sep + dirname + sep + testCase + sep}expected.css'), 'utf8'));\n`+
45+
` expectedTokens = JSON.parse(readFileSync(resolve('test${sep + dirname + sep + testCase + sep}expected.json'), 'utf8'));\n`+
46+
` hook({rootDir: resolve('test${sep + dirname}'), use: pipelines['${dirname}']});\n`+
47+
` });\n`+
48+
`\n`+
49+
` it('loader-core', done => {\n`+
50+
` const loader = new FileSystemLoader(resolve('test${sep + dirname}'), pipelines['${dirname}']);\n`+
51+
`\n`+
52+
` loader.fetch('${testCase + sep}source.css', '/')\n`+
53+
` .then(tokens => {\n`+
54+
` equal(loader.finalSource, expectedCSS);\n`+
55+
` equal(JSON.stringify(tokens), JSON.stringify(expectedTokens));\n`+
56+
` })\n`+
57+
` .then(done, done);\n`+
58+
` });\n`+
59+
`\n`+
60+
` it('require-hook', () => {\n`+
61+
` const tokens = require(resolve('test${sep + dirname + sep + testCase + sep}source.css'));\n`+
62+
` equal(JSON.stringify(tokens), JSON.stringify(expectedTokens));\n`+
63+
` });\n`+
64+
` });\n`;
65+
66+
} else {
67+
68+
content +=
69+
`\n`+
70+
` describe('${testCase.replace(/-/g, ' ')}', () => {\n`+
71+
` before(() => {\n`+
72+
` expectedCSS = normalize(readFileSync(resolve('test${sep + dirname + sep + testCase + sep}expected.css'), 'utf8'));\n`+
73+
` expectedTokens = JSON.parse(readFileSync(resolve('test${sep + dirname + sep + testCase + sep}expected.json'), 'utf8'));\n`+
74+
` hook({rootDir: resolve('test${sep + dirname}'), use: pipelines['${dirname}']});\n`+
75+
` });\n`+
76+
`\n`+
77+
` it('loader-core', done => {\n`+
78+
` const loader = new FileSystemLoader(resolve('test${sep + dirname}'), pipelines['${dirname}']);\n`+
79+
`\n`+
80+
` loader.fetch('${testCase + sep}source1.css', '/').then(tokens1 => {\n`+
81+
` loader.fetch('${testCase + sep}source2.css', '/').then(tokens2 => {\n`+
82+
` equal(loader.finalSource, expectedCSS);\n`+
83+
` const tokens = extend({}, tokens1, tokens2);\n`+
84+
` equal(JSON.stringify(tokens), JSON.stringify(expectedTokens));\n`+
85+
` }).then(done, done);\n`+
86+
` }).catch(done);\n`+
87+
` });\n`+
88+
`\n`+
89+
` it('require-hook', () => {\n`+
90+
` const tokens = extend({},\n`+
91+
` require(resolve('test${sep + dirname + sep + testCase + sep}source1.css')),\n`+
92+
` require(resolve('test${sep + dirname + sep + testCase + sep}source2.css'))\n`+
93+
` );\n`+
94+
`\n`+
95+
` equal(JSON.stringify(tokens), JSON.stringify(expectedTokens));\n`+
96+
` });\n`+
97+
` });\n`;
98+
99+
}
100+
});
101+
102+
content +=
103+
`\n`+
104+
` });\n`;
105+
});
106+
107+
content +=
108+
`\n`+
109+
`});\n`;
110+
111+
writeFile(destination, content, 'utf8', err => {
112+
if (err) {
113+
throw err;
114+
}
115+
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"pretest": "npm run -s lint",
3939
"test": "mocha --compilers js:babel/register",
4040
"test:cov": "`npm bin`/babel-node `npm bin`/isparta cover --report text --report html `npm bin`/_mocha",
41+
"test:gen": "babel-node generate-tests",
4142
"build": "babel src --out-dir dist",
4243
"prepublish": "in-publish && npm run -s build || in-install"
4344
},

src/fn.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @param {*} a
3+
* @return {*}
4+
*/
5+
export function identity(a) {
6+
return a;
7+
}
8+
9+
/**
10+
* @param {string} str
11+
* @return {string}
12+
*/
13+
export function removeQuotes(str) {
14+
return str.replace(/^["']|["']$/g, '');
15+
}

src/index.js

Lines changed: 81 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,125 @@
1-
import './guard';
21
import hook from './hook';
3-
import postcss from 'postcss';
4-
import { dirname, join, parse, relative, resolve, sep } from 'path';
52
import { readFileSync } from 'fs';
6-
import isPlainObject from 'lodash.isplainobject';
3+
import { dirname, sep, relative, resolve } from 'path';
4+
import { identity, removeQuotes } from './fn';
5+
import postcss from 'postcss';
76

87
import ExtractImports from 'postcss-modules-extract-imports';
98
import LocalByDefault from 'postcss-modules-local-by-default';
109
import Scope from 'postcss-modules-scope';
1110
import Parser from './parser';
1211

13-
let processCss;
14-
let rootDir;
15-
let plugins;
12+
// cache
13+
let importNr = 0;
14+
let tokensByFile = {};
15+
// processing functions
16+
const preProcess = identity;
17+
let postProcess;
18+
// defaults
19+
let plugins = [LocalByDefault, ExtractImports, Scope];
20+
let rootDir = process.cwd();
1621

1722
/**
18-
* @param {object} opts
23+
* @param {object} opts
24+
* @param {function} opts.createImportedName
1925
* @param {function} opts.generateScopedName
20-
* @param {function} opts.processCss|.p
21-
* @param {string} opts.rootDir|.root|.d
22-
* @param {array} opts.use|.u
26+
* @param {function} opts.processCss
27+
* @param {string} opts.rootDir
28+
* @param {array} opts.use
2329
*/
24-
export default function buildOptions(opts = {}) {
25-
if (!isPlainObject(opts)) {
26-
throw new Error('Use plain object');
30+
export default function setup(opts = {}) {
31+
// clearing cache
32+
importNr = 0;
33+
tokensByFile = {};
34+
35+
if (opts.processCss && typeof opts.processCss !== 'function') {
36+
throw new Error('should specify function for processCss');
2737
}
2838

29-
processCss = get(opts, 'processCss|p');
30-
rootDir = get(opts, 'rootDir|root|d');
31-
rootDir = rootDir ? resolve(rootDir) : process.cwd();
39+
postProcess = opts.processCss || null;
3240

33-
const customPlugins = get(opts, 'use|u');
34-
if (Array.isArray(customPlugins)) {
35-
return void (plugins = customPlugins);
41+
if (opts.rootDir && typeof opts.rootDir !== 'string') {
42+
throw new Error('should specify string for rootDir');
3643
}
3744

38-
plugins = [];
45+
rootDir = opts.rootDir || process.cwd();
3946

40-
plugins.push(
41-
opts.mode
42-
? new LocalByDefault({mode: opts.mode})
43-
: LocalByDefault
44-
);
45-
46-
plugins.push(
47-
opts.createImportedName
48-
? new ExtractImports({createImportedName: opts.createImportedName})
49-
: ExtractImports
50-
);
51-
52-
plugins.push(
53-
opts.generateScopedName
54-
? new Scope({generateScopedName: opts.generateScopedName})
55-
: Scope
56-
);
57-
}
47+
if (opts.use) {
48+
if (!Array.isArray(opts.use)) {
49+
throw new Error('should specify array for use');
50+
}
5851

59-
const escapedSeparator = sep.replace(/(.)/g, '\\$1');
60-
const relativePathPattern = new RegExp(`^.{1,2}$|^.{1,2}${escapedSeparator}`);
61-
const tokensByFile = {};
62-
let importNr = 0;
52+
return void (plugins = opts.use);
53+
}
6354

64-
/**
65-
* @param {object} object
66-
* @param {string} keys 'a|b|c'
67-
* @return {*}
68-
*/
69-
function get(object, keys) {
70-
let key;
55+
plugins = [];
7156

72-
keys.split('|').some(k => {
73-
if (!object[k]) {
74-
return false;
57+
if (opts.mode) {
58+
if (typeof opts.mode !== 'string') {
59+
throw new Error('should specify string for mode');
7560
}
7661

77-
key = k;
78-
return true;
79-
});
62+
plugins.push(new LocalByDefault({mode: opts.mode}));
63+
} else {
64+
plugins.push(LocalByDefault);
65+
}
8066

81-
return key ? object[key] : null;
82-
}
67+
if (opts.createImportedName) {
68+
if (typeof opts.createImportedName !== 'function') {
69+
throw new Error('should specify function for createImportedName');
70+
}
8371

84-
/**
85-
* @param {string} pathname
86-
* @return {boolean}
87-
*/
88-
function isModule(pathname) {
89-
const parsed = parse(pathname);
90-
return !parsed.root && !relativePathPattern.test(parsed.dir);
91-
}
72+
plugins.push(new ExtractImports({createImportedName: opts.createImportedName}));
73+
} else {
74+
plugins.push(ExtractImports);
75+
}
9276

93-
/**
94-
* @param {string} sourceString The file content
95-
* @param {string} sourcePath
96-
* @param {string} trace
97-
* @param {function} pathFetcher
98-
* @return {object}
99-
*/
100-
function load(sourceString, sourcePath, trace, pathFetcher) {
101-
const lazyResult = postcss(plugins.concat(new Parser({ pathFetcher, trace })))
102-
.process(sourceString, {from: sourcePath});
77+
if (opts.generateScopedName) {
78+
if (typeof opts.generateScopedName !== 'function') {
79+
throw new Error('should specify function for generateScopedName');
80+
}
10381

104-
return { injectableSource: lazyResult.css, exportTokens: lazyResult.root.tokens };
82+
plugins.push(new Scope({generateScopedName: opts.generateScopedName}));
83+
} else {
84+
plugins.push(Scope);
85+
}
10586
}
10687

10788
/**
108-
* @param {string} _newPath
109-
* @param {string} _relativeTo
89+
* @param {string} _newPath Absolute or relative path. Also can be path to the Node.JS module.
90+
* @param {string} _sourcePath Absolute path (relative to root).
11091
* @param {string} _trace
11192
* @return {object}
11293
*/
113-
function fetch(_newPath, _relativeTo, _trace) {
114-
const newPath = _newPath.replace(/^["']|["']$/g, '');
94+
function fetch(_newPath, _sourcePath, _trace) {
11595
const trace = _trace || String.fromCharCode(importNr++);
116-
117-
const relativeDir = dirname(_relativeTo);
118-
const rootRelativePath = resolve(relativeDir, newPath);
119-
let fileRelativePath = resolve(join(rootDir, relativeDir), newPath);
120-
121-
if (isModule(newPath)) {
122-
fileRelativePath = require.resolve(newPath);
123-
}
124-
125-
const tokens = tokensByFile[fileRelativePath];
96+
const newPath = removeQuotes(_newPath);
97+
// getting absolute path to the processing file
98+
const filename = /\w/.test(newPath[0])
99+
? require.resolve(newPath)
100+
: resolve(rootDir + dirname(_sourcePath), newPath);
101+
102+
// checking cache
103+
let tokens = tokensByFile[filename];
126104
if (tokens) {
127105
return tokens;
128106
}
129107

130-
const source = readFileSync(fileRelativePath, 'utf-8');
131-
const { exportTokens, injectableSource } = load(source, rootRelativePath, trace, fetch);
108+
const rootRelativePath = sep + relative(rootDir, filename);
109+
const CSSSource = preProcess(readFileSync(filename, 'utf8'));
110+
111+
const result = postcss(plugins.concat(new Parser({ fetch, trace })))
112+
.process(CSSSource, {from: rootRelativePath})
113+
.root;
132114

133-
tokensByFile[fileRelativePath] = exportTokens;
115+
tokens = result.tokens;
116+
tokensByFile[filename] = tokens;
134117

135-
if (typeof processCss === 'function') {
136-
processCss(injectableSource);
118+
if (postProcess) {
119+
postProcess(result.toResult().css);
137120
}
138121

139-
return exportTokens;
122+
return tokens;
140123
}
141124

142-
// setting defaults
143-
buildOptions();
144-
145-
hook(filename => fetch(`.${sep}${relative(rootDir, filename)}`, '/'));
125+
hook(filename => fetch(filename, sep + relative(rootDir, filename)));

src/parser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default plugin('parser', function parser(opts = {}) {
99
const fetchImport = (importNode, relativeTo, depNr) => {
1010
const file = importNode.selector.match(importRegexp)[1];
1111
const depTrace = opts.trace + String.fromCharCode(depNr);
12-
const exports = opts.pathFetcher(file, relativeTo, depTrace);
12+
const exports = opts.fetch(file, relativeTo, depTrace);
1313

1414
importNode.each(decl => {
1515
if (decl.type === 'decl') {

test/cases/non-relative-imports/expected.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

test/cases/non-relative-imports/source.css

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)