Skip to content

Commit 2440768

Browse files
committed
Move postcss-modules logic to separate class. Stylistic changes on SassTask.
1 parent 407e2ef commit 2440768

3 files changed

Lines changed: 117 additions & 86 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as path from 'path';
2+
3+
import * as postcss from 'postcss';
4+
import * as cssModules from 'postcss-modules';
5+
import * as crypto from 'crypto';
6+
7+
export interface ICSSModules {
8+
getPlugin: () => postcss.AcceptedPlugin;
9+
getCssJSON: () => Object;
10+
}
11+
12+
export default class CSSModules implements ICSSModules {
13+
private _classMap: Object = {};
14+
15+
public getPlugin = () => {
16+
return cssModules({
17+
getJSON: this.saveJSON,
18+
generateScopedName: this.generateScopedName
19+
});
20+
}
21+
22+
public getCssJSON = (): Object => {
23+
return this._classMap;
24+
}
25+
26+
protected saveJSON = (cssFileName: string, json: Object): void => {
27+
this._classMap = json;
28+
}
29+
30+
protected generateScopedName = (name: string, fileName: string, css: string)
31+
: string => {
32+
const fileBaseName: string = path.basename(fileName);
33+
const hash: string = crypto.createHmac('sha1', fileBaseName)
34+
.update(css)
35+
.digest('hex')
36+
.substring(0, 8);
37+
return `${name}_${hash}`;
38+
}
39+
}

core-build/gulp-core-build-sass/src/SassTask.ts

Lines changed: 60 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import * as nodeSass from 'node-sass';
1717
import * as postcss from 'postcss';
1818
import * as CleanCss from 'clean-css';
1919
import * as autoprefixer from 'autoprefixer';
20-
import * as cssModules from 'postcss-modules';
21-
import * as crypto from 'crypto';
20+
import CSSModules, { ICSSModules } from './CSSModules';
2221

2322
export interface ISassTaskConfig {
2423
/**
@@ -27,7 +26,8 @@ export interface ISassTaskConfig {
2726
preamble?: string;
2827

2928
/**
30-
* An optional parameter for text to include at the end of the generated TypeScript file.
29+
* An optional parameter for text to include at the end of the generated
30+
* TypeScript file.
3131
*/
3232
postamble?: string;
3333

@@ -37,47 +37,48 @@ export interface ISassTaskConfig {
3737
sassMatch?: string[];
3838

3939
/**
40-
* If this option is specified, ALL files will be treated as module.sass or module.scss and will
41-
* automatically generate a corresponding TypeScript file. All classes will be
42-
* appended with a hash to help ensure uniqueness on a page. This file can be
43-
* imported directly, and will contain an object describing the mangled class names.
40+
* If this option is specified, ALL files will be treated as module.sass or
41+
* module.scss and will automatically generate a corresponding TypeScript
42+
* file. All classes will be appended with a hash to help ensure uniqueness
43+
* on a page. This file can be imported directly, and will contain an object
44+
* describing the mangled class names.
4445
*/
4546
useCSSModules?: boolean;
4647

4748
/**
48-
* If false, we will set the CSS property naming warning to verbose message while the module generates
49-
* to prevent task exit with exitcode: 1.
50-
* Default value is true
49+
* If false, we will set the CSS property naming warning to verbose message
50+
* while the module generates to prevent task exit with exitcode: 1.
51+
* Default value is true.
5152
*/
5253
warnOnCssInvalidPropertyName?: boolean;
5354

5455
/**
55-
* If true, we will generate a CSS in the lib folder. If false, the CSS is directly embedded
56-
* into the TypeScript file
56+
* If true, we will generate CSS in the lib folder. If false, the CSS is
57+
* directly embedded into the TypeScript file.
5758
*/
5859
dropCssFiles?: boolean;
5960

6061
/**
61-
* If files are matched by sassMatch which do not end in .module.sass or .module.scss, log a warning.
62+
* If files are matched by sassMatch which do not end in .module.sass or
63+
* .module.scss, log a warning.
6264
*/
6365
warnOnNonCSSModules?: boolean;
6466

6567
/**
66-
* If this option is specified, module CSS will be exported using the name provided. If an
67-
* empty value is specified, the styles will be exported using 'export =', rather than a
68-
* named export. By default we use the 'default' export name.
68+
* If this option is specified, module CSS will be exported using the name
69+
* provided. If an empty value is specified, the styles will be exported
70+
* using 'export =', rather than a named export. By default, we use the
71+
* 'default' export name.
6972
*/
7073
moduleExportName?: string;
7174

7275
/**
73-
* Allows the override of the options passed to clean-css. Options such a returnPromise and
74-
* sourceMap will be ignored.
76+
* Allows the override of the options passed to clean-css. Options such a
77+
* returnPromise and sourceMap will be ignored.
7578
*/
7679
cleanCssOptions?: CleanCss.Options;
7780
}
7881

79-
const _classMaps: { [file: string]: Object } = {};
80-
8182
export class SassTask extends GulpTask<ISassTaskConfig> {
8283
public cleanMatch: string[] = [
8384
'src/**/*.sass.ts',
@@ -88,13 +89,6 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
8889
autoprefixer({ browsers: ['> 1%', 'last 2 versions', 'ie >= 10'] })
8990
];
9091

91-
private _modulePostCssAdditionalPlugins: postcss.AcceptedPlugin[] = [
92-
cssModules({
93-
getJSON: this._generateModuleStub.bind(this),
94-
generateScopedName: this.generateScopedName.bind(this)
95-
})
96-
];
97-
9892
constructor() {
9993
super(
10094
'sass',
@@ -127,19 +121,6 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
127121
}).then(() => { /* collapse void[] to void */ });
128122
}
129123

130-
public generateScopedName(name: string, fileName: string, css: string): string {
131-
const fileBaseName: string = path.basename(fileName);
132-
const hash: string = crypto.createHmac('sha1', fileBaseName)
133-
.update(css)
134-
.digest('hex')
135-
.substring(0, 8);
136-
return `${name}_${hash}`;
137-
}
138-
139-
private _generateModuleStub(cssFileName: string, json: Object): void {
140-
_classMaps[cssFileName] = json;
141-
}
142-
143124
private _processFile(filePath: string): Promise<void> {
144125
// Ignore files that start with underscores
145126
if (path.basename(filePath).match(/^\_/)) {
@@ -148,11 +129,16 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
148129

149130
const isFileModuleCss: boolean = !!filePath.match(/\.module\.s(a|c)ss/);
150131
const processAsModuleCss: boolean = isFileModuleCss || !!this.taskConfig.useCSSModules;
132+
const cssModules: ICSSModules = new CSSModules();
151133

152-
if (!isFileModuleCss && !this.taskConfig.useCSSModules && this.taskConfig.warnOnNonCSSModules) {
153-
// If the file doesn't end with .module.scss and we don't treat all files as module-scss, warn
154-
const relativeFilePath: string = path.relative(this.buildConfig.rootPath, filePath);
155-
this.logWarning(`${relativeFilePath}: filename should end with module.sass or module.scss`);
134+
if (!processAsModuleCss && this.taskConfig.warnOnNonCSSModules) {
135+
const relativeFilePath: string = path.relative(
136+
this.buildConfig.rootPath, filePath
137+
);
138+
this.logWarning(
139+
`${relativeFilePath}: filename should end with either .module.sass ` +
140+
`or .module.scss`
141+
);
156142
}
157143

158144
let cssOutputPath: string | undefined = undefined;
@@ -190,10 +176,10 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
190176
};
191177
}
192178

193-
const plugins: postcss.AcceptedPlugin[] = [
194-
...this._postCSSPlugins,
195-
...(processAsModuleCss ? this._modulePostCssAdditionalPlugins : [])
196-
];
179+
const plugins: postcss.AcceptedPlugin[] = [...this._postCSSPlugins];
180+
if (processAsModuleCss) {
181+
plugins.push(cssModules.getPlugin());
182+
}
197183
return postcss(plugins).process(result.css.toString(), options) as PromiseLike<postcss.Result>;
198184
}).then((result: postcss.Result) => {
199185
let cleanCssOptions: CleanCss.Options = { level: 1, returnPromise: true };
@@ -210,44 +196,40 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
210196
result.styles.toString()
211197
];
212198
if (result.sourceMap && !this.buildConfig.production) {
213-
const encodedSourceMap: string = Buffer.from(result.sourceMap.toString()).toString('base64');
214-
generatedFileLines.push(...[
199+
const encodedSourceMap: string = Buffer.from(result.sourceMap.toString())
200+
.toString('base64');
201+
generatedFileLines.push(
215202
`/*# sourceMappingURL=data:application/json;base64,${encodedSourceMap} */`
216-
]);
203+
);
217204
}
218205

219-
FileSystem.writeFile(cssOutputPathAbsolute, generatedFileLines.join(EOL), { ensureFolderExists: true });
206+
FileSystem.writeFile(
207+
cssOutputPathAbsolute,
208+
generatedFileLines.join(EOL),
209+
{ ensureFolderExists: true }
210+
);
220211
}
221212

222213
const scssTsOutputPath: string = `${filePath}.ts`;
223-
const classNames: Object = _classMaps[filePath];
214+
const classMap: Object = cssModules.getCssJSON();
224215
let exportClassNames: string = '';
225216
const content: string | undefined = result.styles;
226217

227-
if (classNames) {
228-
const classNamesLines: string[] = [
229-
'const styles = {'
230-
];
231-
232-
const classKeys: string[] = Object.keys(classNames);
233-
classKeys.forEach((key: string, index: number) => {
234-
const value: string = classNames[key];
235-
let line: string = '';
218+
if (classMap) {
219+
const classKeys: string[] = Object.keys(classMap);
220+
const styleLines: string[] = [];
221+
classKeys.forEach((key: string) => {
222+
const value: string = classMap[key];
236223
if (key.indexOf('-') !== -1) {
237-
const message: string = `The local CSS class '${key}' is not camelCase and will not be type-safe.`;
238-
this.taskConfig.warnOnCssInvalidPropertyName ?
239-
this.logWarning(message) :
224+
const message: string = `The local CSS class '${key}' is not ` +
225+
`camelCase and will not be type-safe.`;
226+
if (this.taskConfig.warnOnCssInvalidPropertyName) {
227+
this.logWarning(message);
228+
} else {
240229
this.logVerbose(message);
241-
line = ` '${key}': '${value}'`;
242-
} else {
243-
line = ` ${key}: '${value}'`;
244-
}
245-
246-
if ((index + 1) <= classKeys.length) {
247-
line += ',';
230+
}
248231
}
249-
250-
classNamesLines.push(line);
232+
styleLines.push(` ${key}: '${value}'`);
251233
});
252234

253235
let exportString: string = 'export default styles;';
@@ -258,17 +240,16 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
258240
// exportString = `export const ${this.taskConfig.moduleExportName} = styles;`;
259241
}
260242

261-
classNamesLines.push(
243+
exportClassNames = [
244+
'const styles = {',
245+
styleLines.join(`,${EOL}`),
262246
'};',
263247
'',
264248
exportString
265-
);
266-
267-
exportClassNames = classNamesLines.join(EOL);
249+
].join(EOL);
268250
}
269251

270252
let lines: string[] = [];
271-
272253
lines.push(this.taskConfig.preamble || '');
273254

274255
if (cssOutputPathAbsolute) {

core-build/gulp-core-build-sass/src/test/SassTask.test.ts renamed to core-build/gulp-core-build-sass/src/test/CSSModules.test.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@
33
import * as path from 'path';
44
import { expect } from 'chai';
55

6-
import { SassTask } from '../SassTask';
6+
import CSSModules from '../CSSModules';
77

88
interface IScopedNameArgs {
99
name: string;
1010
fileName: string;
1111
css: string;
1212
}
1313

14+
interface ITestCSSModules {
15+
testGenerateScopedName: (name: string, fileName: string, css: string) => string;
16+
}
17+
18+
class TestCSSModules extends CSSModules {
19+
public testGenerateScopedName = (name: string, fileName: string, css: string)
20+
: string => {
21+
return this.generateScopedName(name, fileName, css);
22+
}
23+
}
24+
1425
describe('class name hashing', () => {
1526
it('will generate different hashes for different content', (done) => {
1627
const version1: IScopedNameArgs = {
@@ -23,11 +34,11 @@ describe('class name hashing', () => {
2334
fileName: path.join(__dirname, 'Sally', 'src', 'main.sass'),
2435
css: 'color: pink;'
2536
};
26-
const sassTask: SassTask = new SassTask();
27-
const output1: string = sassTask.generateScopedName(
37+
const cssModules: ITestCSSModules = new TestCSSModules();
38+
const output1: string = cssModules.testGenerateScopedName(
2839
version1.name, version1.fileName, version1.css
2940
);
30-
const output2: string = sassTask.generateScopedName(
41+
const output2: string = cssModules.testGenerateScopedName(
3142
version2.name, version2.fileName, version2.css
3243
);
3344
expect(output1).to.not.equal(output2);
@@ -45,11 +56,11 @@ describe('class name hashing', () => {
4556
fileName: path.join(__dirname, 'Suzan', 'workspace', 'src', 'main.sass'),
4657
css: 'color: blue;'
4758
};
48-
const sassTask: SassTask = new SassTask();
49-
const output1: string = sassTask.generateScopedName(
59+
const cssModules: ITestCSSModules = new TestCSSModules();
60+
const output1: string = cssModules.testGenerateScopedName(
5061
version1.name, version1.fileName, version1.css
5162
);
52-
const output2: string = sassTask.generateScopedName(
63+
const output2: string = cssModules.testGenerateScopedName(
5364
version2.name, version2.fileName, version2.css
5465
);
5566
expect(output1).to.equal(output2);

0 commit comments

Comments
 (0)