diff --git a/__tests__/cli.test.ts b/__tests__/cli.test.ts index 09e4a649..63b79acd 100644 --- a/__tests__/cli.test.ts +++ b/__tests__/cli.test.ts @@ -1,10 +1,12 @@ -import { execSync } from "child_process"; +import { execSync, spawnSync } from "child_process"; describe("cli", () => { it("should run when no files are found", () => { - const result = execSync("npm run typed-scss-modules src").toString(); + const result = spawnSync("npm run typed-scss-modules src", { + shell: true, + }); - expect(result).toContain("No files found."); + expect(result.stderr.toString()).toContain("No files found."); }); describe("examples", () => { diff --git a/__tests__/core/alerts.test.ts b/__tests__/core/alerts.test.ts index ec7c5ef0..ccff9104 100644 --- a/__tests__/core/alerts.test.ts +++ b/__tests__/core/alerts.test.ts @@ -2,13 +2,16 @@ import { alerts, setAlertsLogLevel } from "../../lib/core"; describe("alerts", () => { let logSpy: jest.SpyInstance; + let warnSpy: jest.SpyInstance; beforeEach(() => { logSpy = jest.spyOn(console, "log").mockImplementation(); + warnSpy = jest.spyOn(console, "warn").mockImplementation(); }); afterEach(() => { logSpy.mockRestore(); + warnSpy.mockRestore(); }); const TEST_ALERT_MSG = "TEST ALERT MESSAGE"; @@ -20,29 +23,29 @@ describe("alerts", () => { alerts.error(TEST_ALERT_MSG); - expect(console.log).toHaveBeenLastCalledWith(EXPECTED); + expect(console.warn).toHaveBeenLastCalledWith(EXPECTED); //make sure each alert only calls console.log once - expect(console.log).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledTimes(1); alerts.warn(TEST_ALERT_MSG); - expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(2); + expect(console.warn).toHaveBeenLastCalledWith(EXPECTED); + expect(console.warn).toHaveBeenCalledTimes(2); alerts.notice(TEST_ALERT_MSG); expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(3); + expect(console.log).toHaveBeenCalledTimes(1); alerts.info(TEST_ALERT_MSG); expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(4); + expect(console.log).toHaveBeenCalledTimes(2); alerts.success(TEST_ALERT_MSG); expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(5); + expect(console.log).toHaveBeenCalledTimes(3); }); it("should only print error messages with error log level", () => { @@ -50,8 +53,8 @@ describe("alerts", () => { alerts.error(TEST_ALERT_MSG); - expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenLastCalledWith(EXPECTED); + expect(console.warn).toHaveBeenCalledTimes(1); alerts.warn(TEST_ALERT_MSG); alerts.notice(TEST_ALERT_MSG); @@ -59,7 +62,7 @@ describe("alerts", () => { alerts.success(TEST_ALERT_MSG); //shouldn't change - expect(console.log).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledTimes(1); }); it("should print all but warning messages with info log level", () => { @@ -67,27 +70,27 @@ describe("alerts", () => { alerts.error(TEST_ALERT_MSG); - expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenLastCalledWith(EXPECTED); + expect(console.warn).toHaveBeenCalledTimes(1); alerts.notice(TEST_ALERT_MSG); expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(2); + expect(console.log).toHaveBeenCalledTimes(1); alerts.info(TEST_ALERT_MSG); expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(3); + expect(console.log).toHaveBeenCalledTimes(2); alerts.success(TEST_ALERT_MSG); expect(console.log).toHaveBeenLastCalledWith(EXPECTED); - expect(console.log).toHaveBeenCalledTimes(4); + expect(console.log).toHaveBeenCalledTimes(3); alerts.warn(TEST_ALERT_MSG); - expect(console.log).toHaveBeenCalledTimes(4); + expect(console.warn).toHaveBeenCalledTimes(1); }); it("should print no messages with silent log level", () => { @@ -100,5 +103,6 @@ describe("alerts", () => { alerts.success(TEST_ALERT_MSG); expect(console.log).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); }); }); diff --git a/__tests__/core/generate.test.ts b/__tests__/core/generate.test.ts index 4edbcdba..9652d09d 100644 --- a/__tests__/core/generate.test.ts +++ b/__tests__/core/generate.test.ts @@ -27,9 +27,16 @@ describeAllImplementations((implementation) => { updateStaleOnly: false, logLevel: "verbose", outputFolder: null, + aliases: { + "~fancy-import": "complex", + "~another": "style", + }, + aliasPrefixes: { + "~": "nested-styles/", + }, }); - expect(fs.writeFileSync).toHaveBeenCalledTimes(6); + expect(fs.writeFileSync).toHaveBeenCalledTimes(9); }); }); }); diff --git a/__tests__/core/list-different.test.ts b/__tests__/core/list-different.test.ts index e87b4fed..dc0a6145 100644 --- a/__tests__/core/list-different.test.ts +++ b/__tests__/core/list-different.test.ts @@ -7,6 +7,7 @@ describeAllImplementations((implementation) => { beforeEach(() => { console.log = jest.fn(); + console.warn = jest.fn(); exit = jest.spyOn(process, "exit").mockImplementation(); }); @@ -41,10 +42,10 @@ describeAllImplementations((implementation) => { }); expect(exit).toHaveBeenCalledWith(1); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining(`[INVALID TYPES] Check type definitions for`) ); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining(`invalid.scss`) ); }); @@ -69,8 +70,8 @@ describeAllImplementations((implementation) => { outputFolder: null, }); - expect(console.log).toHaveBeenCalledTimes(1); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining(`Only 1 file found for`) ); expect(exit).not.toHaveBeenCalled(); @@ -96,6 +97,7 @@ describeAllImplementations((implementation) => { }); expect(exit).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); expect(console.log).not.toHaveBeenCalled(); }); @@ -119,12 +121,12 @@ describeAllImplementations((implementation) => { }); expect(exit).toHaveBeenCalledWith(1); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining( `[INVALID TYPES] Type file needs to be generated for` ) ); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining(`no-generated.scss`) ); }); @@ -149,8 +151,8 @@ describeAllImplementations((implementation) => { }); expect(exit).not.toHaveBeenCalled(); - expect(console.log).toHaveBeenCalledTimes(1); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining(`No files found`) ); }); diff --git a/__tests__/core/list-files-and-perform-sanity-check.test.ts b/__tests__/core/list-files-and-perform-sanity-check.test.ts index e4723de3..91c097aa 100644 --- a/__tests__/core/list-files-and-perform-sanity-check.test.ts +++ b/__tests__/core/list-files-and-perform-sanity-check.test.ts @@ -19,7 +19,7 @@ const options: ConfigOptions = { describe("listAllFilesAndPerformSanityCheck", () => { beforeEach(() => { - console.log = jest.fn(); + console.warn = jest.fn(); }); it("prints a warning if the pattern matches 0 files", () => { @@ -27,7 +27,7 @@ describe("listAllFilesAndPerformSanityCheck", () => { listFilesAndPerformSanityChecks(pattern, options); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining("No files found.") ); }); @@ -37,7 +37,7 @@ describe("listAllFilesAndPerformSanityCheck", () => { listFilesAndPerformSanityChecks(pattern, options); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining("Only 1 file found for") ); }); diff --git a/__tests__/dummy-styles/global-variables.scss b/__tests__/dummy-styles/global-variables.scss index 33c36c85..71ee34f4 100644 --- a/__tests__/dummy-styles/global-variables.scss +++ b/__tests__/dummy-styles/global-variables.scss @@ -1,3 +1,5 @@ +$global-red: #ff0000; + .globalStyle { background-color: $global-red; } diff --git a/__tests__/typescript/class-names-to-type-definitions.test.ts b/__tests__/typescript/class-names-to-type-definitions.test.ts index 4575ee18..5c922c09 100644 --- a/__tests__/typescript/class-names-to-type-definitions.test.ts +++ b/__tests__/typescript/class-names-to-type-definitions.test.ts @@ -7,7 +7,7 @@ jest.mock("../../lib/prettier/can-resolve", () => ({ describe("classNamesToTypeDefinitions (without Prettier)", () => { beforeEach(() => { - console.log = jest.fn(); + console.warn = jest.fn(); }); describe("named", () => { @@ -41,7 +41,7 @@ describe("classNamesToTypeDefinitions (without Prettier)", () => { }); expect(definition).toEqual("export declare const myClass: string;\n"); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining(`[SKIPPING] 'if' is a reserved keyword`) ); }); @@ -54,7 +54,7 @@ describe("classNamesToTypeDefinitions (without Prettier)", () => { }); expect(definition).toEqual("export declare const myClass: string;\n"); - expect(console.log).toHaveBeenCalledWith( + expect(console.warn).toHaveBeenCalledWith( expect.stringContaining(`[SKIPPING] 'invalid-variable' contains dashes`) ); }); diff --git a/lib/cli.ts b/lib/cli.ts index 50bb3722..6a3074be 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import yargs from "yargs"; +import { alerts } from "./core"; import { IMPLEMENTATIONS } from "./implementations"; import { main } from "./main"; import { Aliases, NAME_FORMATS } from "./sass"; @@ -145,4 +146,8 @@ const { _: patterns, ...rest } = yargs .parseSync(); // eslint-disable-next-line @typescript-eslint/no-floating-promises -main(patterns[0] as string, { ...rest }); +main(patterns[0] as string, { ...rest }).catch((error: Error) => { + alerts.error("Encountered an error while generating type definitions."); + alerts.error(error); + process.exitCode = 1; +}); diff --git a/lib/core/alerts.ts b/lib/core/alerts.ts index 1d80e483..f9e2582b 100644 --- a/lib/core/alerts.ts +++ b/lib/core/alerts.ts @@ -1,4 +1,5 @@ import chalk from "chalk"; +import { SassError } from "node-sass"; export const LOG_LEVELS = ["verbose", "error", "info", "silent"] as const; export type LogLevel = (typeof LOG_LEVELS)[number]; @@ -32,12 +33,35 @@ const withLogLevelsRestriction = } }; +const normalizeErrorMessage = (error: string | Error) => { + if (error && error instanceof Error) { + if ("file" in error) { + const { message, file, line, column } = error as SassError; + const location = file ? ` (${file}[${line}:${column}])` : ""; + const wrappedError = new Error(`SASS Error ${location}\n${message}`, { + cause: error, + }); + + wrappedError.stack = chalk.red(wrappedError.stack); + + return wrappedError; + } + + const wrappedError = new Error(error.message); + wrappedError.stack = chalk.red(error.stack); + return wrappedError; + } + + return chalk.red(error); +}; const error = withLogLevelsRestriction( ["verbose", "error", "info"], - (message: string) => console.log(chalk.red(message)) + (message: string | Error) => { + console.warn(normalizeErrorMessage(message)); + } ); const warn = withLogLevelsRestriction(["verbose"], (message: string) => - console.log(chalk.yellowBright(message)) + console.warn(chalk.yellowBright(message)) ); const notice = withLogLevelsRestriction( ["verbose", "info"], diff --git a/lib/core/write-file.ts b/lib/core/write-file.ts index c6beb23e..a2eebd75 100644 --- a/lib/core/write-file.ts +++ b/lib/core/write-file.ts @@ -1,5 +1,4 @@ import fs from "fs"; -import { SassError } from "node-sass"; import path from "path"; import { fileToClassNames } from "../sass"; import { @@ -20,57 +19,51 @@ export const writeFile = async ( file: string, options: CLIOptions ): Promise => { - try { - const classNames = await fileToClassNames(file, options); - const typeDefinition = await classNamesToTypeDefinitions({ - classNames, - ...options, - }); + const classNames = await fileToClassNames(file, options); + const typeDefinition = await classNamesToTypeDefinitions({ + classNames, + ...options, + }); - const typesPath = getTypeDefinitionPath(file, options); - const typesExist = fs.existsSync(typesPath); + const typesPath = getTypeDefinitionPath(file, options); + const typesExist = fs.existsSync(typesPath); - // Avoid outputting empty type definition files. - // If the file exists and the type definition is now empty, remove the file. - if (!typeDefinition) { - if (typesExist) { - removeSCSSTypeDefinitionFile(file, options); - } else { - alerts.notice(`[NO GENERATED TYPES] ${file}`); - } - return; + // Avoid outputting empty type definition files. + // If the file exists and the type definition is now empty, remove the file. + if (!typeDefinition) { + if (typesExist) { + removeSCSSTypeDefinitionFile(file, options); + } else { + alerts.notice(`[NO GENERATED TYPES] ${file}`); } + return; + } - // Avoid re-writing the file if it hasn't changed. - // First by checking the file modification time, then - // by comparing the file contents. - if (options.updateStaleOnly && typesExist) { - const fileModified = fs.statSync(file).mtime; - const typeDefinitionModified = fs.statSync(typesPath).mtime; - - if (fileModified < typeDefinitionModified) { - return; - } + // Avoid re-writing the file if it hasn't changed. + // First by checking the file modification time, then + // by comparing the file contents. + if (options.updateStaleOnly && typesExist) { + const fileModified = fs.statSync(file).mtime; + const typeDefinitionModified = fs.statSync(typesPath).mtime; - const existingTypeDefinition = fs.readFileSync(typesPath, "utf8"); - if (existingTypeDefinition === typeDefinition) { - return; - } + if (fileModified < typeDefinitionModified) { + return; } - // Files can be written to arbitrary directories and need to - // be nested to match the project structure so it's possible - // there are multiple directories that need to be created. - const dirname = path.dirname(typesPath); - if (!fs.existsSync(dirname)) { - fs.mkdirSync(dirname, { recursive: true }); + const existingTypeDefinition = fs.readFileSync(typesPath, "utf8"); + if (existingTypeDefinition === typeDefinition) { + return; } + } - fs.writeFileSync(typesPath, typeDefinition); - alerts.success(`[GENERATED TYPES] ${typesPath}`); - } catch (error) { - const { message, file, line, column } = error as SassError; - const location = file ? ` (${file}[${line}:${column}])` : ""; - alerts.error(`${message}${location}`); + // Files can be written to arbitrary directories and need to + // be nested to match the project structure so it's possible + // there are multiple directories that need to be created. + const dirname = path.dirname(typesPath); + if (!fs.existsSync(dirname)) { + fs.mkdirSync(dirname, { recursive: true }); } + + fs.writeFileSync(typesPath, typeDefinition); + alerts.success(`[GENERATED TYPES] ${typesPath}`); }; diff --git a/lib/load.ts b/lib/load.ts index 7c590b6c..569895a6 100644 --- a/lib/load.ts +++ b/lib/load.ts @@ -1,7 +1,7 @@ import { bundleRequire } from "bundle-require"; import JoyCon from "joycon"; import path from "path"; -import { alerts, CLIOptions, ConfigOptions } from "./core"; +import { CLIOptions, ConfigOptions } from "./core"; import { getDefaultImplementation } from "./implementations"; import { nameFormatDefault } from "./sass"; import { @@ -39,25 +39,16 @@ export const loadConfig = async (): Promise< ); if (configPath) { - try { - const configModule = await bundleRequire({ - filepath: configPath, - }); + const configModule = await bundleRequire({ + filepath: configPath, + }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const config: ConfigOptions = - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - configModule.mod.config || configModule.mod.default || configModule.mod; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const config: ConfigOptions = + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + configModule.mod.config || configModule.mod.default || configModule.mod; - return config; - } catch (error) { - alerts.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `An error occurred loading the config file "${configPath}":\n${error}` - ); - - return {}; - } + return config; } return {}; diff --git a/tsconfig.json b/tsconfig.json index dd46268b..c6c26a91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "ES2022", "module": "commonjs", "strict": true, "esModuleInterop": true,