diff --git a/packages/react-dev-utils/__tests__/getCSSModuleLocalIdent.test.js b/packages/react-dev-utils/__tests__/getCSSModuleLocalIdent.test.js index 6eb189eef85..a0b6c3661dc 100644 --- a/packages/react-dev-utils/__tests__/getCSSModuleLocalIdent.test.js +++ b/packages/react-dev-utils/__tests__/getCSSModuleLocalIdent.test.js @@ -11,7 +11,11 @@ const getCSSModuleLocalIdent = require('../getCSSModuleLocalIdent'); const rootContext = '/path'; const defaultClassName = 'class'; -const options = { context: undefined, hashPrefix: '', regExp: null }; +const defaultOptions = { + context: undefined, + hashSalt: undefined, + regExp: null, +}; const tests = [ { @@ -26,16 +30,60 @@ const tests = [ resourcePath: '/path/to/file.module.sass', expected: 'file_class__dINZX', }, + { + resourcePath: '/path/to/file.module.sass', + expected: 'file_class__9vVbt', + options: { + hashSalt: 'my-app', + }, + }, { resourcePath: '/path/to/file.name.module.css', expected: 'file_name_class__XpUJW', }, + { + resourcePath: '/path/to/file.name.module.css', + expected: 'file_name_class__OS1Yg', + options: { + hashSalt: 'my-app', + }, + }, + { + resourcePath: '/path/to/file.name.module.css', + expected: 'file_name_class__uMbcn', + options: { + hashSalt: 'my-app-123', + }, + }, + { + resourcePath: '/path/a/b/c/file.name.module.css', + className: 'test', + expected: 'file_name_test__2F_aq', + description: + 'Verifies that hash is encoded with base64url (2F_aq instead of 2F/aq)', + }, + { + resourcePath: '/path/a/b/file.name.module.css', + className: 'test', + expected: 'file_name_test__58Gc-', + description: + 'Verifies that hash is encoded with base64url (58Gc- instead of 58Gc+)', + }, ]; describe('getCSSModuleLocalIdent', () => { tests.forEach(test => { - const { className = defaultClassName, expected, resourcePath } = test; - it(JSON.stringify({ resourcePath, className }), () => { + const { + className = defaultClassName, + expected, + resourcePath, + options = defaultOptions, + description, + } = test; + const testDescription = + description || JSON.stringify({ resourcePath, className }); + + it(testDescription, () => { const ident = getCSSModuleLocalIdent( { resourcePath, diff --git a/packages/react-dev-utils/getCSSModuleLocalIdent.js b/packages/react-dev-utils/getCSSModuleLocalIdent.js index ce25305e7ef..944b9634de6 100644 --- a/packages/react-dev-utils/getCSSModuleLocalIdent.js +++ b/packages/react-dev-utils/getCSSModuleLocalIdent.js @@ -22,11 +22,15 @@ module.exports = function getLocalIdent( ) ? '[folder]' : '[name]'; - // Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique. + // Create a hash based on the relative file location, class name and hashSalt (if given). + // Will be unique across a project and globally unique with a suitable hashSalt. + const hashSalt = options.hashSalt || ''; const hash = loaderUtils.getHashDigest( - path.posix.relative(context.rootContext, context.resourcePath) + localName, + hashSalt + + path.posix.relative(context.rootContext, context.resourcePath) + + localName, 'md5', - 'base64', + 'base64url', 5 ); // Use loaderUtils to find the file or folder name diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index 2b1b3bbd47d..034c8024722 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -95,6 +95,8 @@ const hasJsxRuntime = (() => { } })(); +const appName = require(paths.appPackageJson).name; + // This is the production and development configuration. // It is focused on developer experience, fast rebuilds, and a minimal bundle. module.exports = function (webpackEnv) { @@ -537,6 +539,7 @@ module.exports = function (webpackEnv) { modules: { mode: 'local', getLocalIdent: getCSSModuleLocalIdent, + localIdentHashSalt: appName, }, }), }, @@ -577,6 +580,7 @@ module.exports = function (webpackEnv) { modules: { mode: 'local', getLocalIdent: getCSSModuleLocalIdent, + localIdentHashSalt: appName, }, }, 'sass-loader'