Skip to content

Commit

Permalink
Merge branch 'master' into ianc/improved-change-error
Browse files Browse the repository at this point in the history
  • Loading branch information
iclanton authored May 31, 2019
2 parents 94663e0 + 2263e70 commit 59f1dbd
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 98 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ temp
# Rush files
common/temp/**
package-deps.json

# OS X
.DS_Store
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/gulp-core-build-sass",
"comment": "Make css modules class hash names consistent relative to root path.",
"type": "minor"
}
],
"packageName": "@microsoft/gulp-core-build-sass",
"email": "[email protected]"
}
3 changes: 3 additions & 0 deletions core-build/gulp-core-build-sass/config/jest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"isEnabled": true
}
2 changes: 1 addition & 1 deletion core-build/gulp-core-build-sass/gulpfile.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';

let build = require('@microsoft/node-library-build');
build.mocha.enabled = false;

build.initialize(require('gulp'));
6 changes: 4 additions & 2 deletions core-build/gulp-core-build-sass/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"url": "https://github.com/Microsoft/web-build-tools/tree/master/core-build/gulp-core-build-sass"
},
"scripts": {
"build": "gulp --clean"
"build": "gulp test --clean"
},
"dependencies": {
"@microsoft/gulp-core-build": "3.9.26",
Expand All @@ -34,6 +34,8 @@
"@types/glob": "5.0.30",
"@types/node-sass": "3.10.32",
"@types/clean-css": "4.2.1",
"gulp": "~3.9.1"
"gulp": "~3.9.1",
"@types/jest": "23.3.11",
"jest": "~23.6.0"
}
}
70 changes: 70 additions & 0 deletions core-build/gulp-core-build-sass/src/CSSModules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as path from 'path';

import * as postcss from 'postcss';
import * as cssModules from 'postcss-modules';
import * as crypto from 'crypto';

export interface IClassMap {
[className: string]: string;
}

export interface ICSSModules {
/**
* Return a configured postcss plugin that will map class names to a
* consistently generated scoped name.
*/
getPlugin(): postcss.AcceptedPlugin;

/**
* Return the CSS class map that is stored after postcss-modules runs.
*/
getClassMap(): IClassMap;
}

export default class CSSModules implements ICSSModules {
private _classMap: IClassMap;
private _rootPath: string;

/**
* CSSModules includes the source file's path relative to the project root
* as part of the class name hashing algorithm.
* This should be configured with the setting:
* {@link @microsoft/gulp-core-build#IBuildConfig.rootPath}
* That is used in {@link ./SassTask#SassTask}
* But will default the process' current working dir.
*/
constructor(rootPath?: string) {
this._classMap = {};
if (rootPath) {
this._rootPath = rootPath;
} else {
this._rootPath = process.cwd();
}
}

public getPlugin(): postcss.AcceptedPlugin {
return cssModules({
getJSON: this.saveJson.bind(this),
generateScopedName: this.generateScopedName.bind(this)
});
}

public getClassMap(): IClassMap {
return this._classMap;
}

protected saveJson(cssFileName: string, json: IClassMap): void {
this._classMap = json;
}

protected generateScopedName(name: string, fileName: string, css: string)
: string {
const fileBaseName: string = path.relative(this._rootPath, fileName);
const safeFileBaseName: string = fileBaseName.replace(/\\/g, '/');
const hash: string = crypto.createHmac('sha1', safeFileBaseName)
.update(css)
.digest('hex')
.substring(0, 8);
return `${name}_${hash}`;
}
}
173 changes: 79 additions & 94 deletions core-build/gulp-core-build-sass/src/SassTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import * as nodeSass from 'node-sass';
import * as postcss from 'postcss';
import * as CleanCss from 'clean-css';
import * as autoprefixer from 'autoprefixer';
import * as cssModules from 'postcss-modules';
import * as crypto from 'crypto';
import CSSModules, { ICSSModules, IClassMap } from './CSSModules';

export interface ISassTaskConfig {
/**
Expand All @@ -27,7 +26,8 @@ export interface ISassTaskConfig {
preamble?: string;

/**
* An optional parameter for text to include at the end of the generated TypeScript file.
* An optional parameter for text to include at the end of the generated
* TypeScript file.
*/
postamble?: string;

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

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

/**
* If false, we will set the CSS property naming warning to verbose message while the module generates
* to prevent task exit with exitcode: 1.
* Default value is true
* If false, we will set the CSS property naming warning to verbose message
* while the module generates to prevent task exit with exitcode: 1.
* Default value is true.
*/
warnOnCssInvalidPropertyName?: boolean;

/**
* If true, we will generate a CSS in the lib folder. If false, the CSS is directly embedded
* into the TypeScript file
* If true, we will generate CSS in the lib folder. If false, the CSS is
* directly embedded into the TypeScript file.
*/
dropCssFiles?: boolean;

/**
* If files are matched by sassMatch which do not end in .module.sass or .module.scss, log a warning.
* If files are matched by sassMatch which do not end in .module.sass or
* .module.scss, log a warning.
*/
warnOnNonCSSModules?: boolean;

/**
* If this option is specified, module CSS will be exported using the name provided. If an
* empty value is specified, the styles will be exported using 'export =', rather than a
* named export. By default we use the 'default' export name.
* If this option is specified, module CSS will be exported using the name
* provided. If an empty value is specified, the styles will be exported
* using 'export =', rather than a named export. By default, we use the
* 'default' export name.
*/
moduleExportName?: string;

/**
* Allows the override of the options passed to clean-css. Options such a returnPromise and
* sourceMap will be ignored.
* Allows the override of the options passed to clean-css. Options such a
* returnPromise and sourceMap will be ignored.
*/
cleanCssOptions?: CleanCss.Options;
}

const _classMaps: { [file: string]: Object } = {};

export class SassTask extends GulpTask<ISassTaskConfig> {
public cleanMatch: string[] = [
'src/**/*.sass.ts',
Expand All @@ -88,13 +89,6 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
autoprefixer({ browsers: ['> 1%', 'last 2 versions', 'ie >= 10'] })
];

private _modulePostCssAdditionalPlugins: postcss.AcceptedPlugin[] = [
cssModules({
getJSON: this._generateModuleStub.bind(this),
generateScopedName: this._generateScopedName.bind(this)
})
];

constructor() {
super(
'sass',
Expand Down Expand Up @@ -127,14 +121,6 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
}).then(() => { /* collapse void[] to void */ });
}

private _generateModuleStub(cssFileName: string, json: Object): void {
_classMaps[cssFileName] = json;
}

private _generateScopedName(name: string, fileName: string, css: string): string {
return name + '_' + crypto.createHmac('sha1', fileName).update(css).digest('hex').substring(0, 8);
}

private _processFile(filePath: string): Promise<void> {
// Ignore files that start with underscores
if (path.basename(filePath).match(/^\_/)) {
Expand All @@ -143,11 +129,15 @@ export class SassTask extends GulpTask<ISassTaskConfig> {

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

if (!isFileModuleCss && !this.taskConfig.useCSSModules && this.taskConfig.warnOnNonCSSModules) {
// If the file doesn't end with .module.scss and we don't treat all files as module-scss, warn
const relativeFilePath: string = path.relative(this.buildConfig.rootPath, filePath);
this.logWarning(`${relativeFilePath}: filename should end with module.sass or module.scss`);
if (!processAsModuleCss && this.taskConfig.warnOnNonCSSModules) {
const relativeFilePath: string = path.relative(
this.buildConfig.rootPath, filePath
);
this.logWarning(
`${relativeFilePath}: filename should end with module.sass or module.scss`
);
}

let cssOutputPath: string | undefined = undefined;
Expand Down Expand Up @@ -185,10 +175,10 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
};
}

const plugins: postcss.AcceptedPlugin[] = [
...this._postCSSPlugins,
...(processAsModuleCss ? this._modulePostCssAdditionalPlugins : [])
];
const plugins: postcss.AcceptedPlugin[] = [...this._postCSSPlugins];
if (processAsModuleCss) {
plugins.push(cssModules.getPlugin());
}
return postcss(plugins).process(result.css.toString(), options) as PromiseLike<postcss.Result>;
}).then((result: postcss.Result) => {
let cleanCssOptions: CleanCss.Options = { level: 1, returnPromise: true };
Expand All @@ -206,76 +196,36 @@ export class SassTask extends GulpTask<ISassTaskConfig> {
];
if (result.sourceMap && !this.buildConfig.production) {
const encodedSourceMap: string = Buffer.from(result.sourceMap.toString()).toString('base64');
generatedFileLines.push(...[
generatedFileLines.push(
`/*# sourceMappingURL=data:application/json;base64,${encodedSourceMap} */`
]);
);
}

FileSystem.writeFile(cssOutputPathAbsolute, generatedFileLines.join(EOL), { ensureFolderExists: true });
FileSystem.writeFile(
cssOutputPathAbsolute,
generatedFileLines.join(EOL),
{ ensureFolderExists: true }
);
}

const scssTsOutputPath: string = `${filePath}.ts`;
const classNames: Object = _classMaps[filePath];
let exportClassNames: string = '';
const classMap: IClassMap = cssModules.getClassMap();
const stylesExportString: string = this._getStylesExportString(classMap);
const content: string | undefined = result.styles;

if (classNames) {
const classNamesLines: string[] = [
'const styles = {'
];

const classKeys: string[] = Object.keys(classNames);
classKeys.forEach((key: string, index: number) => {
const value: string = classNames[key];
let line: string = '';
if (key.indexOf('-') !== -1) {
const message: string = `The local CSS class '${key}' is not camelCase and will not be type-safe.`;
this.taskConfig.warnOnCssInvalidPropertyName ?
this.logWarning(message) :
this.logVerbose(message);
line = ` '${key}': '${value}'`;
} else {
line = ` ${key}: '${value}'`;
}

if ((index + 1) <= classKeys.length) {
line += ',';
}

classNamesLines.push(line);
});

let exportString: string = 'export default styles;';

if (this.taskConfig.moduleExportName === '') {
exportString = 'export = styles;';
} else if (!!this.taskConfig.moduleExportName) {
// exportString = `export const ${this.taskConfig.moduleExportName} = styles;`;
}

classNamesLines.push(
'};',
'',
exportString
);

exportClassNames = classNamesLines.join(EOL);
}

let lines: string[] = [];

lines.push(this.taskConfig.preamble || '');

if (cssOutputPathAbsolute) {
lines = lines.concat([
`require(${JSON.stringify(`./${path.basename(cssOutputPathAbsolute)}`)});`,
exportClassNames
stylesExportString
]);
} else if (!!content) {
lines = lines.concat([
'import { loadStyles } from \'@microsoft/load-themed-styles\';',
'',
exportClassNames,
stylesExportString,
'',
`loadStyles(${JSON.stringify(splitStyles(content))});`
]);
Expand Down Expand Up @@ -322,4 +272,39 @@ export class SassTask extends GulpTask<ISassTaskConfig> {

return url;
}

private _getStylesExportString(classMap: IClassMap): string {
const classKeys: string[] = Object.keys(classMap);
const styleLines: string[] = [];
classKeys.forEach((key: string) => {
const value: string = classMap[key];
if (key.indexOf('-') !== -1) {
const message: string = `The local CSS class '${key}' is not ` +
`camelCase and will not be type-safe.`;
if (this.taskConfig.warnOnCssInvalidPropertyName) {
this.logWarning(message);
} else {
this.logVerbose(message);
}
key = `'${key}'`;
}
styleLines.push(` ${key}: '${value}'`);
});

let exportString: string = 'export default styles;';

if (this.taskConfig.moduleExportName === '') {
exportString = 'export = styles;';
} else if (!!this.taskConfig.moduleExportName) {
// exportString = `export const ${this.taskConfig.moduleExportName} = styles;`;
}

return [
'const styles = {',
styleLines.join(`,${EOL}`),
'};',
'',
exportString
].join(EOL);
}
}
Loading

0 comments on commit 59f1dbd

Please sign in to comment.