diff --git a/src/utils.js b/src/utils.js index 39a85979..4a1d1cc6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -111,6 +111,14 @@ function getFilter(filter, resourcePath) { }; } +function getValidLocalName(localName, exportLocalsConvention) { + if (exportLocalsConvention === 'dashesOnly') { + return dashesCamelCase(localName); + } + + return camelCase(localName); +} + const moduleRegExp = /\.module(s)?\.\w+$/i; const icssRegExp = /\.icss\.\w+$/i; @@ -203,9 +211,12 @@ function getModulesOptions(rawOptions, loaderContext) { ); } - if (modulesOptions.exportLocalsConvention !== 'camelCaseOnly') { + if ( + modulesOptions.exportLocalsConvention !== 'camelCaseOnly' && + modulesOptions.exportLocalsConvention !== 'dashesOnly' + ) { throw new Error( - 'The "modules.namedExport" option requires the "modules.exportLocalsConvention" option to be "camelCaseOnly"' + 'The "modules.namedExport" option requires the "modules.exportLocalsConvention" option to be "camelCaseOnly" or "dashesOnly"' ); } } @@ -516,7 +527,10 @@ function getModuleCode(result, api, replacements, options, loaderContext) { code = code.replace(new RegExp(replacementName, 'g'), () => options.modules.namedExport ? `" + ${importName}_NAMED___[${JSON.stringify( - camelCase(localName) + getValidLocalName( + localName, + options.modules.exportLocalsConvention + ) )}] + "` : `" + ${importName}.locals[${JSON.stringify(localName)}] + "` ); @@ -551,9 +565,7 @@ function getExportCode(exports, replacements, options) { const addExportToLocalsCode = (name, value) => { if (options.modules.namedExport) { - localsCode += `export const ${camelCase(name)} = ${JSON.stringify( - value - )};\n`; + localsCode += `export const ${name} = ${JSON.stringify(value)};\n`; } else { if (localsCode) { localsCode += `,\n`; @@ -609,7 +621,7 @@ function getExportCode(exports, replacements, options) { localsCode = localsCode.replace(new RegExp(replacementName, 'g'), () => { if (options.modules.namedExport) { return `" + ${importName}_NAMED___[${JSON.stringify( - camelCase(localName) + getValidLocalName(localName, options.modules.exportLocalsConvention) )}] + "`; } else if (options.modules.exportOnlyLocals) { return `" + ${importName}[${JSON.stringify(localName)}] + "`; diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap index 9aba4b85..c2b4c4b5 100644 --- a/test/__snapshots__/modules-option.test.js.snap +++ b/test/__snapshots__/modules-option.test.js.snap @@ -1614,7 +1614,7 @@ exports[`"modules" option should throw an error when class has unsupported name exports[`"modules" option should throw an error when the "namedExport" is enabled and the "exportLocalsConvention" options has not "camelCaseOnly" value: errors 1`] = ` Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): -Error: The \\"modules.namedExport\\" option requires the \\"modules.exportLocalsConvention\\" option to be \\"camelCaseOnly\\"", +Error: The \\"modules.namedExport\\" option requires the \\"modules.exportLocalsConvention\\" option to be \\"camelCaseOnly\\" or \\"dashesOnly\\"", ] `; @@ -1629,6 +1629,15 @@ Error: The \\"modules.namedExport\\" option requires the \\"esModules\\" option exports[`"modules" option should throw an error when the "namedExport" option is "true", but the "esModule" is "false": warnings 1`] = `Array []`; +exports[`"modules" option should throw error with composes when the "namedExport" is enabled and "exportLocalsConvention" options has invalid value: errors 1`] = ` +Array [ + "ModuleBuildError: Module build failed (from \`replaced original path\`): +Error: The \\"modules.namedExport\\" option requires the \\"modules.exportLocalsConvention\\" option to be \\"camelCaseOnly\\" or \\"dashesOnly\\"", +] +`; + +exports[`"modules" option should throw error with composes when the "namedExport" is enabled and "exportLocalsConvention" options has invalid value: warnings 1`] = `Array []`; + exports[`"modules" option should work and correctly replace escaped symbols: errors 1`] = `Array []`; exports[`"modules" option should work and correctly replace escaped symbols: module 1`] = ` @@ -3719,6 +3728,39 @@ Object { exports[`"modules" option should work js template with "namedExport" option: warnings 1`] = `Array []`; +exports[`"modules" option should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value: errors 1`] = `Array []`; + +exports[`"modules" option should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value: module 1`] = ` +"// Imports +import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \\".foo_barBaz {\\\\n color: red;\\\\n}\\\\n\\\\n.bar {\\\\n color: red;\\\\n}\\\\n\\", \\"\\"]); +// Exports +export const foo_barBaz = \\"foo_barBaz\\"; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value: result 1`] = ` +Array [ + Array [ + "./modules/namedExport/dashesOnly/index.css", + ".foo_barBaz { + color: red; +} + +.bar { + color: red; +} +", + "", + ], +] +`; + +exports[`"modules" option should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value: warnings 1`] = `Array []`; + exports[`"modules" option should work with "exportOnlyLocals" and "esModule" with "false" value options: errors 1`] = `Array []`; exports[`"modules" option should work with "exportOnlyLocals" and "esModule" with "false" value options: module 1`] = ` @@ -12152,6 +12194,67 @@ Array [ exports[`"modules" option should work with case \`values-10\` (\`modules\` value is \`true)\`: warnings 1`] = `Array []`; +exports[`"modules" option should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value: errors 1`] = `Array []`; + +exports[`"modules" option should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value: module 1`] = ` +"// Imports +import ___CSS_LOADER_API_IMPORT___ from \\"../../../../../src/runtime/api.js\\"; +import ___CSS_LOADER_ICSS_IMPORT_0___, * as ___CSS_LOADER_ICSS_IMPORT_0____NAMED___ from \\"-!../../../../../src/index.js??[ident]!./values.css\\"; +var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]}); +___CSS_LOADER_EXPORT___.i(___CSS_LOADER_ICSS_IMPORT_0___, \\"\\", true); +// Module +___CSS_LOADER_EXPORT___.push([module.id, \\"._ghi {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def\\"] + \\";\\\\n}\\\\n\\\\n._my-class {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"sWhite\\"] + \\";\\\\n}\\\\n\\\\n._other {\\\\n display: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"mSmall\\"] + \\";\\\\n}\\\\n\\\\n._other-other {\\\\n width: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def\\"] + \\";\\\\n}\\\\n\\\\n._green {\\\\n color: \\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_otherOther\\"] + \\";\\\\n}\\\\n\\", \\"\\"]); +// Exports +export const v_def = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_def\\"] + \\"\\"; +export const v_otherOther = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"v_otherOther\\"] + \\"\\"; +export const sWhite = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"sWhite\\"] + \\"\\"; +export const mSmall = \\"\\" + ___CSS_LOADER_ICSS_IMPORT_0____NAMED___[\\"mSmall\\"] + \\"\\"; +export const ghi = \\"_ghi\\"; +export const myClass = \\"_my-class\\"; +export const other = \\"_other\\"; +export const otherOther = \\"_other-other\\"; +export const green = \\"_green\\"; +export default ___CSS_LOADER_EXPORT___; +" +`; + +exports[`"modules" option should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value: result 1`] = ` +Array [ + Array [ + "../../src/index.js?[ident]!./modules/namedExport/composes/values.css", + " +", + "", + ], + Array [ + "./modules/namedExport/composes/composes.css", + "._ghi { + color: red; +} + +._my-class { + color: white; +} + +._other { + display: (min-width: 320px); +} + +._other-other { + width: red; +} + +._green { + color: green; +} +", + "", + ], +] +`; + +exports[`"modules" option should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value: warnings 1`] = `Array []`; + exports[`"modules" option should work with the "[local]" placeholder for the "localIdentName" option: errors 1`] = `Array []`; exports[`"modules" option should work with the "[local]" placeholder for the "localIdentName" option: module 1`] = ` diff --git a/test/fixtures/modules/namedExport/composes/composes.css b/test/fixtures/modules/namedExport/composes/composes.css new file mode 100644 index 00000000..7a7d4fa9 --- /dev/null +++ b/test/fixtures/modules/namedExport/composes/composes.css @@ -0,0 +1,24 @@ +@value v_def from './values.css'; +@value v_other-other from './values.css'; +@value s-white from './values.css'; +@value m-small from './values.css'; + +.ghi { + color: v_def; +} + +.my-class { + color: s-white; +} + +.other { + display: m-small; +} + +.other-other { + width: v_def; +} + +.green { + color: v_other-other; +} diff --git a/test/fixtures/modules/namedExport/composes/composes.js b/test/fixtures/modules/namedExport/composes/composes.js new file mode 100644 index 00000000..ba922e3f --- /dev/null +++ b/test/fixtures/modules/namedExport/composes/composes.js @@ -0,0 +1,5 @@ +import css from './composes.css'; + +__export__ = css; + +export default css; diff --git a/test/fixtures/modules/namedExport/composes/values.css b/test/fixtures/modules/namedExport/composes/values.css new file mode 100644 index 00000000..075f5246 --- /dev/null +++ b/test/fixtures/modules/namedExport/composes/values.css @@ -0,0 +1,4 @@ +@value v_def: red; +@value v_other-other: green; +@value s-white: white; +@value m-small: (min-width: 320px); diff --git a/test/fixtures/modules/namedExport/dashesOnly/index.css b/test/fixtures/modules/namedExport/dashesOnly/index.css new file mode 100644 index 00000000..6c1ec23d --- /dev/null +++ b/test/fixtures/modules/namedExport/dashesOnly/index.css @@ -0,0 +1,7 @@ +:local(.foo_barBaz) { + color: red; +} + +:global(.bar) { + color: red; +} diff --git a/test/fixtures/modules/namedExport/dashesOnly/index.js b/test/fixtures/modules/namedExport/dashesOnly/index.js new file mode 100644 index 00000000..e1a19303 --- /dev/null +++ b/test/fixtures/modules/namedExport/dashesOnly/index.js @@ -0,0 +1,5 @@ +import css from './index.css'; + +__export__ = css; + +export default css; diff --git a/test/modules-option.test.js b/test/modules-option.test.js index 1bb121ae..d442c6cd 100644 --- a/test/modules-option.test.js +++ b/test/modules-option.test.js @@ -1203,6 +1203,60 @@ describe('"modules" option', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); + it('should work when the "namedExport" is enabled and the "exportLocalsConvention" options has "dashesOnly" value', async () => { + const compiler = getCompiler('./modules/namedExport/dashesOnly/index.js', { + modules: { + localIdentName: '[local]', + namedExport: true, + exportLocalsConvention: 'dashesOnly', + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource('./modules/namedExport/dashesOnly/index.css', stats) + ).toMatchSnapshot('module'); + expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot( + 'result' + ); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats, true)).toMatchSnapshot('errors'); + }); + + it('should work with composes when the "namedExport" is enabled and "exportLocalsConvention" options has "dashesOnly" value', async () => { + const compiler = getCompiler('./modules/namedExport/composes/composes.js', { + modules: { + localIdentName: '_[local]', + namedExport: true, + exportLocalsConvention: 'dashesOnly', + }, + }); + const stats = await compile(compiler); + + expect( + getModuleSource('./modules/namedExport/composes/composes.css', stats) + ).toMatchSnapshot('module'); + expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot( + 'result' + ); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should throw error with composes when the "namedExport" is enabled and "exportLocalsConvention" options has invalid value', async () => { + const compiler = getCompiler('./modules/namedExport/composes/composes.js', { + modules: { + localIdentName: '_[local]', + namedExport: true, + exportLocalsConvention: 'dashes', + }, + }); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats, true)).toMatchSnapshot('errors'); + }); + it('should throw an error when the "namedExport" option is "true", but the "esModule" is "false"', async () => { const compiler = getCompiler('./modules/namedExport/base/index.js', { esModule: false,