diff --git a/README.md b/README.md index d1cf037..c213a12 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ module.exports = { | **[`verifyOnly`](#verifyOnly)** | `{Boolean}` | Validate generated `*.d.ts` files and fail if an update is needed (useful in CI) | | **[`disableLocalsExport`](#disableLocalsExport)** | `{Boolean}` | Disable the use of locals export. | | **[`prettierConfigFile`](#prettierConfigFile)** | `{String}` | Path to prettier config file | +| **[`namedExport`](#namedExport)** | `{Boolean}` | Interpret `namedExport` of css-loader. | ### `banner` @@ -217,6 +218,37 @@ module.exports = { }; ``` +### `namedExport` + +Interpret `namedExport` of css-loader. + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + { + loader: "@teamsupercell/typings-for-css-modules-loader", + options: { + namedExport: true, + } + }, + { + loader: "css-loader", + options: { + modules: { + namedExport: true, + } + } + } + ] + } + ] + } +}; +``` ## Example diff --git a/src/index.js b/src/index.js index 04b6b99..f8d109e 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,8 @@ const { filenameToPascalCase, filenameToTypingsFilename, - getCssModuleKeys, + getCssModuleKeysForLocalExport, + getCssModuleKeysForNamedExport, generateGenericExportInterface, } = require("./utils"); const persist = require("./persist"); @@ -40,6 +41,10 @@ const schema = { description: "Path to prettier config file", type: "string", + }, + namedExport: { + description: "Interpret `namedExport` of css-loader", + type: "boolean", } }, additionalProperties: false, @@ -61,13 +66,21 @@ module.exports = function (content, ...args) { this.cacheable(); } - // let's only check `exports.locals` for keys to avoid getting keys from the sourcemap when it's enabled - // if we cannot find locals, then the module only contains global styles - const indexOfLocals = content.indexOf(".locals"); - const cssModuleKeys = - indexOfLocals === -1 - ? [] - : getCssModuleKeys(content.substring(indexOfLocals)); + let cssModuleKeys = []; + + if (options.namedExport) { + const indexOfExports = content.indexOf("// Exports"); + if (indexOfExports !== -1) { + cssModuleKeys = getCssModuleKeysForNamedExport(content.substring(indexOfExports)) + } + } else { + // let's only check `exports.locals` for keys to avoid getting keys from the sourcemap when it's enabled + // if we cannot find locals, then the module only contains global styles + const indexOfLocals = content.indexOf(".locals"); + if (indexOfLocals !== -1) { + cssModuleKeys = getCssModuleKeysForLocalExport(content.substring(indexOfLocals)); + } + } /** @type {any} */ const callback = this.async(); diff --git a/src/utils.js b/src/utils.js index 3454110..56edbb7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,11 +3,10 @@ const path = require("path"); const camelCase = require("camelcase"); /** - * @param {string} content - * @returns {string[]} + * @param {RegExp} keyRegex + * @returns {(content: string) => string[]} */ -const getCssModuleKeys = (content) => { - const keyRegex = /"([\w-]+)":/g; +const getCssModuleKeys = (keyRegex) => (content) => { let match; const cssModuleKeys = []; @@ -19,6 +18,12 @@ const getCssModuleKeys = (content) => { return cssModuleKeys; }; +/** @type {(content: string) => string[]} */ +const getCssModuleKeysForLocalExport = getCssModuleKeys(/"([\w-]+)":/g); + +/** @type {(content: string) => string[]} */ +const getCssModuleKeysForNamedExport = getCssModuleKeys(/export const ([\w-]+) =/g); + /** * @param {string} filename */ @@ -79,7 +84,8 @@ export = ${moduleName};`; }; module.exports = { - getCssModuleKeys, + getCssModuleKeysForLocalExport, + getCssModuleKeysForNamedExport, filenameToPascalCase, filenameToTypingsFilename, generateGenericExportInterface, diff --git a/test/__snapshots__/index.test.js.snap b/test/__snapshots__/index.test.js.snap index 069a0a3..d495ef3 100644 --- a/test/__snapshots__/index.test.js.snap +++ b/test/__snapshots__/index.test.js.snap @@ -283,3 +283,21 @@ declare const ExampleCssModule: ExampleCssNamespace.IExampleCss & { export = ExampleCssModule; " `; + +exports[`css-loader@latest with named export 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + barBaz: string; + composed: string; + foo: string; + } +} + +declare const ExampleCssModule: ExampleCssNamespace.IExampleCss & { + /** WARNING: Only available when \`css-loader\` is used without \`style-loader\` or \`mini-css-extract-plugin\` */ + locals: ExampleCssNamespace.IExampleCss; +}; + +export = ExampleCssModule; +" +`; diff --git a/test/index.test.js b/test/index.test.js index d2032d1..63ab786 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -147,6 +147,23 @@ describe("css-loader@latest", () => { const verifyMock = jest.requireMock("../src/verify"); expect(verifyMock).toBeCalledTimes(1); }); + + it("with named export", async () => { + await runTest({ + options: { + namedExport: true, + }, + cssLoaderOptions: { + modules: { + namedExport: true, + } + } + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); }); describe("css-loader@3", () => { diff --git a/test/utils.test.js b/test/utils.test.js index 8170bdd..fe27503 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,5 +1,9 @@ // @ts-check -const { filenameToPascalCase, getCssModuleKeys } = require("../src/utils"); +const { + filenameToPascalCase, + getCssModuleKeysForLocalExport, + getCssModuleKeysForNamedExport, +} = require("../src/utils"); describe("filenameToPascalCase", () => { it("camelCase", () => { @@ -23,22 +27,38 @@ describe("filenameToPascalCase", () => { }); }); -describe("getCssModuleKeys", () => { +describe.each` + description | fn + ${"localExport"} | ${getCssModuleKeysForLocalExport} + ${"namedExport"} | ${getCssModuleKeysForNamedExport} +`("getCssModuleKeys / common / $description", ({ fn }) => { it("empty CSS module", () => { const content = ` exports = module.exports = require("../node_modules/css-loader/dist/runtime/api.js")(false); // Module exports.push([module.id, "", ""]); `; - const actual = getCssModuleKeys(content); + const actual = fn(content); expect(actual).toEqual([]); }); + it("CSS module with :root pseudo-class only", () => { + const content = ` + exports = module.exports = require("../node_modules/css-loader/dist/runtime/api.js")(false); + // Module + exports.push([module.id, ":root {\n --background: green; }\n", ""]); + `; + const actual = fn(content); + expect(actual).toEqual([]); + }); +}); + +describe("getCssModuleKeysForLocalExport", () => { it("CSS module with one class", () => { const content = `exports.locals = { "test": "test" };`; - const actual = getCssModuleKeys(content); + const actual = getCssModuleKeysForLocalExport(content); expect(actual).toEqual(["test"]); }); @@ -47,17 +67,26 @@ describe("getCssModuleKeys", () => { "test1": "test1", "test2": "test2" };`; - const actual = getCssModuleKeys(content); + const actual = getCssModuleKeysForLocalExport(content); expect(actual).toEqual(["test1", "test2"]); }); +}); - it("CSS module with :root pseudo-class only", () => { +describe("getCssModuleKeysForNamedExport", () => { + it("CSS module with one class", () => { const content = ` - exports = module.exports = require("../node_modules/css-loader/dist/runtime/api.js")(false); - // Module - exports.push([module.id, ":root {\n --background: green; }\n", ""]); + export const test = "test"; `; - const actual = getCssModuleKeys(content); - expect(actual).toEqual([]); + const actual = getCssModuleKeysForNamedExport(content); + expect(actual).toEqual(["test"]); + }); + + it("CSS module with multiple classes", () => { + const content = ` + export const test1 = "test1"; + export const test2 = "test2"; + `; + const actual = getCssModuleKeysForNamedExport(content); + expect(actual).toEqual(["test1", "test2"]); }); });