diff --git a/.travis.yml b/.travis.yml index 247ab71..775ad1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,17 @@ language: node_js + node_js: - "10" + branches: only: - master + +jobs: + include: + - install: npm ci + - install: npm install --no-shrinkwrap + +before_script: + - npm ls css-loader + - npm ls typescript diff --git a/package-lock.json b/package-lock.json index 079f3db..b3a0826 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1950,7 +1950,68 @@ } }, "css-loader": { - "version": "3.6.0", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.2.1.tgz", + "integrity": "sha512-MoqmF1if7Z0pZIEXA4ZF9PgtCXxWbfzfJM+3p+OYfhcrwcqhaCRb74DSnfzRl7e024xEiCRn5hCvfUbTf2sgFA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^2.0.0", + "normalize-path": "^3.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.3", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^7.3.2" + }, + "dependencies": { + "camelcase": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", + "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "css-loader3": { + "version": "npm:css-loader@3.6.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", "dev": true, diff --git a/package.json b/package.json index 3d2a857..1141db7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ }, "devDependencies": { "@types/jest": "^24.0.23", - "css-loader": "^3.1.0", + "css-loader": "*", + "css-loader3": "npm:css-loader@^3.1.0", "eslint": "4.18.2", "eslint-config-prettier": "^6.0.0", "jest": "^24.9.0", diff --git a/src/index.js b/src/index.js index 9f2af79..19febfb 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ const { filenameToPascalCase, filenameToTypingsFilename, getCssModuleKeys, - generateGenericExportInterface + generateGenericExportInterface, } = require("./utils"); const persist = require("./persist"); const verify = require("./verify"); @@ -16,39 +16,38 @@ const schema = { eol: { description: "Newline character to be used in generated d.ts files. Uses OS default. This option is overridden by the formatter option.", - type: "string" + type: "string", }, banner: { description: "To add a 'banner' prefix to each generated `*.d.ts` file", - type: "string" + type: "string", }, formatter: { description: "Possible options: none and prettier (requires prettier package installed). Defaults to prettier if `prettier` module can be resolved", - enum: ["prettier", "none"] + enum: ["prettier", "none"], }, disableLocalsExport: { - description: - "Disable the use of locals export. Defaults to `false`", - type: "boolean" + description: "Disable the use of locals export. Defaults to `false`", + type: "boolean", }, verifyOnly: { description: "Validate generated `*.d.ts` files and fail if an update is needed (useful in CI). Defaults to `false`", - type: "boolean" - } + type: "boolean", + }, }, - additionalProperties: false + additionalProperties: false, }; /** @type {any} */ const configuration = { name: "typings-for-css-modules-loader", - baseDataPath: "options" + baseDataPath: "options", }; /** @type {((this: import('webpack').loader.LoaderContext, ...args: any[]) => void) & {pitch?: import('webpack').loader.Loader['pitch']}} */ -module.exports = function(content, ...args) { +module.exports = function (content, ...args) { const options = getOptions(this) || {}; validateOptions(schema, options, configuration); @@ -58,9 +57,12 @@ module.exports = function(content, ...args) { } // let's only check `exports.locals` for keys to avoid getting keys from the sourcemap when it's enabled - const cssModuleKeys = getCssModuleKeys( - content.substring(content.indexOf("exports.locals")) - ); + // 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)); /** @type {any} */ const callback = this.async(); @@ -85,14 +87,14 @@ module.exports = function(content, ...args) { ); applyFormattingAndOptions(cssModuleDefinition, options) - .then(output => { + .then((output) => { if (options.verifyOnly === true) { return verify(cssModuleInterfaceFilename, output); } else { persist(cssModuleInterfaceFilename, output); } }) - .catch(err => { + .catch((err) => { this.emitError(err); }) .then(successfulCallback); @@ -132,7 +134,7 @@ async function applyPrettier(input) { const prettier = require("prettier"); const config = await prettier.resolveConfig("./", { - editorconfig: true + editorconfig: true, }); return prettier.format( diff --git a/src/utils.js b/src/utils.js index 17a0c6b..3454110 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,7 +6,7 @@ const camelCase = require("camelcase"); * @param {string} content * @returns {string[]} */ -const getCssModuleKeys = content => { +const getCssModuleKeys = (content) => { const keyRegex = /"([\w-]+)":/g; let match; const cssModuleKeys = []; @@ -22,7 +22,7 @@ const getCssModuleKeys = content => { /** * @param {string} filename */ -const filenameToPascalCase = filename => { +const filenameToPascalCase = (filename) => { return camelCase(path.basename(filename), { pascalCase: true }); }; @@ -33,11 +33,11 @@ const filenameToPascalCase = filename => { const cssModuleToTypescriptInterfaceProperties = (cssModuleKeys, indent) => { return [...cssModuleKeys] .sort() - .map(key => `${indent || ""}'${key}': string;`) + .map((key) => `${indent || ""}'${key}': string;`) .join("\n"); }; -const filenameToTypingsFilename = filename => { +const filenameToTypingsFilename = (filename) => { const dirName = path.dirname(filename); const baseName = path.basename(filename); return path.join(dirName, `${baseName}.d.ts`); @@ -47,12 +47,18 @@ const filenameToTypingsFilename = filename => { * @param {string[]} cssModuleKeys * @param {string} pascalCaseFileName */ -const generateGenericExportInterface = (cssModuleKeys, pascalCaseFileName, disableLocalsExport) => { +const generateGenericExportInterface = ( + cssModuleKeys, + pascalCaseFileName, + disableLocalsExport +) => { const interfaceName = `I${pascalCaseFileName}`; const moduleName = `${pascalCaseFileName}Module`; const namespaceName = `${pascalCaseFileName}Namespace`; - const localsExportType = disableLocalsExport ? `` : ` & { + const localsExportType = disableLocalsExport + ? `` + : ` & { /** WARNING: Only available when \`css-loader\` is used without \`style-loader\` or \`mini-css-extract-plugin\` */ locals: ${namespaceName}.${interfaceName}; }`; @@ -76,5 +82,5 @@ module.exports = { getCssModuleKeys, filenameToPascalCase, filenameToTypingsFilename, - generateGenericExportInterface + generateGenericExportInterface, }; diff --git a/src/verify.js b/src/verify.js index 65d1c7a..a584d0e 100644 --- a/src/verify.js +++ b/src/verify.js @@ -1,6 +1,6 @@ // @ts-check -const fs = require('fs'); -const util = require('util'); +const fs = require("fs"); +const util = require("util"); const fsStat = util.promisify(fs.stat); const fsReadFile = util.promisify(fs.readFile); /** @@ -10,18 +10,23 @@ const fsReadFile = util.promisify(fs.readFile); */ module.exports = async (filename, content) => { const fileExists = await fsStat(filename) - .then(() => true).catch(() => false); + .then(() => true) + .catch(() => false); if (!fileExists) { - throw new Error(`Verification failed: Generated typings for css-module file '${filename}' is not found. ` + - "It typically happens when the generated typings were not committed."); + throw new Error( + `Verification failed: Generated typings for css-module file '${filename}' is not found. ` + + "It typically happens when the generated typings were not committed." + ); } - const existingFileContent = await fsReadFile(filename, 'utf-8'); + const existingFileContent = await fsReadFile(filename, "utf-8"); // let's not fail the build if there are whitespace changes only if (existingFileContent.replace(/\s+/g, "") !== content.replace(/\s+/g, "")) { - throw new Error(`Verification failed: Generated typings for css-modules file '${filename}' is out of date. ` + - "It typically happens when the up-to-date generated typings are not committed."); + throw new Error( + `Verification failed: Generated typings for css-modules file '${filename}' is out of date. ` + + "It typically happens when the up-to-date generated typings are not committed." + ); } }; diff --git a/test/__snapshots__/index.test.js.snap b/test/__snapshots__/index.test.js.snap index edbbb14..069a0a3 100644 --- a/test/__snapshots__/index.test.js.snap +++ b/test/__snapshots__/index.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`default options 1`] = ` +exports[`css-loader@3 default options 1`] = ` "declare namespace ExampleCssNamespace { export interface IExampleCss { \\"bar-baz\\": string; @@ -18,7 +18,7 @@ export = ExampleCssModule; " `; -exports[`localsConvention asIs 1`] = ` +exports[`css-loader@3 localsConvention asIs 1`] = ` "declare namespace ExampleCssNamespace { export interface IExampleCss { \\"bar-baz\\": string; @@ -36,7 +36,7 @@ export = ExampleCssModule; " `; -exports[`localsConvention camelCase 1`] = ` +exports[`css-loader@3 localsConvention camelCase 1`] = ` "declare namespace ExampleCssNamespace { export interface IExampleCss { \\"bar-baz\\": string; @@ -55,7 +55,7 @@ export = ExampleCssModule; " `; -exports[`with banner 1`] = ` +exports[`css-loader@3 with banner 1`] = ` "// autogenerated by typings-for-css-modules-loader declare namespace ExampleCssNamespace { export interface IExampleCss { @@ -74,7 +74,7 @@ export = ExampleCssModule; " `; -exports[`with locals export disabled 1`] = ` +exports[`css-loader@3 with locals export disabled 1`] = ` "declare namespace ExampleCssNamespace { export interface IExampleCss { \\"bar-baz\\": string; @@ -89,7 +89,7 @@ export = ExampleCssModule; " `; -exports[`with no formatter 1`] = ` +exports[`css-loader@3 with no formatter 1`] = ` "declare namespace ExampleCssNamespace { export interface IExampleCss { 'bar-baz': string; @@ -106,7 +106,7 @@ declare const ExampleCssModule: ExampleCssNamespace.IExampleCss & { export = ExampleCssModule;" `; -exports[`with prettier 1`] = ` +exports[`css-loader@3 with prettier 1`] = ` "declare namespace ExampleCssNamespace { export interface IExampleCss { \\"bar-baz\\": string; @@ -124,7 +124,149 @@ export = ExampleCssModule; " `; -exports[`with sourcemap 1`] = ` +exports[`css-loader@3 with sourcemap 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + \\"bar-baz\\": 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; +" +`; + +exports[`css-loader@latest default options 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + \\"bar-baz\\": 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; +" +`; + +exports[`css-loader@latest localsConvention asIs 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + \\"bar-baz\\": 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; +" +`; + +exports[`css-loader@latest localsConvention camelCase 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + \\"bar-baz\\": string; + 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; +" +`; + +exports[`css-loader@latest with banner 1`] = ` +"// autogenerated by typings-for-css-modules-loader +declare namespace ExampleCssNamespace { + export interface IExampleCss { + \\"bar-baz\\": 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; +" +`; + +exports[`css-loader@latest with locals export disabled 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + \\"bar-baz\\": string; + composed: string; + foo: string; + } +} + +declare const ExampleCssModule: ExampleCssNamespace.IExampleCss; + +export = ExampleCssModule; +" +`; + +exports[`css-loader@latest with no formatter 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + 'bar-baz': 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;" +`; + +exports[`css-loader@latest with prettier 1`] = ` +"declare namespace ExampleCssNamespace { + export interface IExampleCss { + \\"bar-baz\\": 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; +" +`; + +exports[`css-loader@latest with sourcemap 1`] = ` "declare namespace ExampleCssNamespace { export interface IExampleCss { \\"bar-baz\\": string; diff --git a/test/example-no-locals.css b/test/example-no-locals.css new file mode 100644 index 0000000..6380c4c --- /dev/null +++ b/test/example-no-locals.css @@ -0,0 +1,3 @@ +div { + background: red; +} diff --git a/test/index.test.js b/test/index.test.js index 8b2dca4..d2032d1 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -9,168 +9,330 @@ beforeEach(() => { jest.mock("../src/verify"); }); -it("default options", async () => { - await runTest(); +describe("css-loader@latest", () => { + const runTest = createTestRunner(); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + it("default options", async () => { + await runTest(); - const verifyMock = jest.requireMock("../src/verify"); - expect(verifyMock).toBeCalledTimes(0); -}); + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -it("with sourcemap", async () => { - await runTest({ - cssLoaderOptions: { - sourceMap: true - } + const verifyMock = jest.requireMock("../src/verify"); + expect(verifyMock).toBeCalledTimes(0); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -}); + it("with sourcemap", async () => { + await runTest({ + cssLoaderOptions: { + sourceMap: true, + }, + }); -it("no modules", async () => { - await runTest({ - cssLoaderOptions: { - modules: false - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(0); -}); + it("no locals in output", async () => { + await runTest({ + fileName: "./example-no-locals.css", + cssLoaderOptions: { + sourceMap: true, + }, + }); -it("localsConvention asIs", async () => { - await runTest({ - cssLoaderOptions: { - localsConvention: "asIs" - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(0); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -}); + it("no modules", async () => { + await runTest({ + cssLoaderOptions: { + modules: false, + }, + }); -it("localsConvention camelCase", async () => { - await runTest({ - cssLoaderOptions: { - localsConvention: "camelCase" - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(0); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -}); + it("localsConvention asIs", async () => { + await runTest({ + cssLoaderOptions: { + modules: { + exportLocalsConvention: "asIs", + }, + }, + }); -it("with prettier", async () => { - await runTest({ - options: { - formatter: "prettier" - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -}); + it("localsConvention camelCase", async () => { + await runTest({ + cssLoaderOptions: { + modules: { + exportLocalsConvention: "camelCase", + }, + }, + }); -it("with no formatter", async () => { - await runTest({ - options: { - formatter: "none" - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -}); + it("with prettier", async () => { + await runTest({ + options: { + formatter: "prettier", + }, + }); -it("with banner", async () => { - await runTest({ - options: { - banner: "// autogenerated by typings-for-css-modules-loader" - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -}); + it("with no formatter", async () => { + await runTest({ + options: { + formatter: "none", + }, + }); -it("with locals export disabled", async () => { - await runTest({ - options: { - disableLocalsExport: true - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(1); - expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); -}); + it("with banner", async () => { + await runTest({ + options: { + banner: "// autogenerated by typings-for-css-modules-loader", + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); + + it("with locals export disabled", async () => { + await runTest({ + options: { + disableLocalsExport: true, + }, + }); -it("with verify only", async () => { - await runTest({ - options: { - verifyOnly: true - } + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); }); - const persistMock = jest.requireMock("../src/persist"); - expect(persistMock).toBeCalledTimes(0); + it("with verify only", async () => { + await runTest({ + options: { + verifyOnly: true, + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(0); - const verifyMock = jest.requireMock("../src/verify"); - expect(verifyMock).toBeCalledTimes(1); + const verifyMock = jest.requireMock("../src/verify"); + expect(verifyMock).toBeCalledTimes(1); + }); }); -async function runTest({ options = {}, cssLoaderOptions = {} } = {}) { - const compiler = webpack({ - entry: path.resolve(__dirname, "./example.css"), - target: "node", - module: { - rules: [ - { - test: /\.css$/, - use: [ - { - loader: require.resolve("../src/index.js"), - options - }, - { - loader: "css-loader", - options: Object.assign( - { - modules: true - }, - cssLoaderOptions - ) - } - ] - } - ] - }, - mode: "none" +describe("css-loader@3", () => { + const runTest = createTestRunner("css-loader3"); + + it("default options", async () => { + await runTest(); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + + const verifyMock = jest.requireMock("../src/verify"); + expect(verifyMock).toBeCalledTimes(0); + }); + + it("with sourcemap", async () => { + await runTest({ + cssLoaderOptions: { + sourceMap: true, + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); + + it("no locals in output", async () => { + await runTest({ + fileName: "./example-no-locals.css", + cssLoaderOptions: { + sourceMap: true, + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(0); }); - compiler.outputFileSystem = new memoryfs(); + it("no modules", async () => { + await runTest({ + cssLoaderOptions: { + modules: false, + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(0); + }); + + it("localsConvention asIs", async () => { + await runTest({ + cssLoaderOptions: { + localsConvention: "asIs", + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); + + it("localsConvention camelCase", async () => { + await runTest({ + cssLoaderOptions: { + localsConvention: "camelCase", + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); + + it("with prettier", async () => { + await runTest({ + options: { + formatter: "prettier", + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); + + it("with no formatter", async () => { + await runTest({ + options: { + formatter: "none", + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); + + it("with banner", async () => { + await runTest({ + options: { + banner: "// autogenerated by typings-for-css-modules-loader", + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); + + it("with locals export disabled", async () => { + await runTest({ + options: { + disableLocalsExport: true, + }, + }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(1); + expect(persistMock.mock.calls[0][1]).toMatchSnapshot(); + }); - /** @type {webpack.Stats} */ - const stats = await new Promise((resolve, reject) => { - compiler.run((err, stats) => { - if (err) { - reject(err); - } else { - resolve(stats); - } + it("with verify only", async () => { + await runTest({ + options: { + verifyOnly: true, + }, }); + + const persistMock = jest.requireMock("../src/persist"); + expect(persistMock).toBeCalledTimes(0); + + const verifyMock = jest.requireMock("../src/verify"); + expect(verifyMock).toBeCalledTimes(1); }); +}); + +function createTestRunner(cssLoaderModule = "css-loader") { + return async ({ + fileName = "./example.css", + options = {}, + cssLoaderOptions = {}, + } = {}) => { + const compiler = webpack({ + entry: path.resolve(__dirname, fileName), + target: "node", + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: require.resolve("../src/index.js"), + options, + }, + { + loader: cssLoaderModule, + options: Object.assign( + { + modules: true, + }, + cssLoaderOptions + ), + }, + ], + }, + ], + }, + mode: "none", + }); + + compiler.outputFileSystem = new memoryfs(); + + /** @type {webpack.Stats} */ + const stats = await new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + reject(err); + } else { + resolve(stats); + } + }); + }); - const s = stats.toJson(); - expect(s.errors).toHaveLength(0); + const s = stats.toJson(); + expect(s.errors).toHaveLength(0); + }; } diff --git a/test/utils.test.js b/test/utils.test.js index 4ad272c..8170bdd 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -37,18 +37,18 @@ describe("getCssModuleKeys", () => { it("CSS module with one class", () => { const content = `exports.locals = { "test": "test" - };` + };`; const actual = getCssModuleKeys(content); - expect(actual).toEqual(['test']); + expect(actual).toEqual(["test"]); }); it("CSS module with multiple classes", () => { const content = `exports.locals = { "test1": "test1", "test2": "test2" - };` + };`; const actual = getCssModuleKeys(content); - expect(actual).toEqual(['test1', 'test2']); + expect(actual).toEqual(["test1", "test2"]); }); it("CSS module with :root pseudo-class only", () => {