From a9f69a22a66e8bf12e3119042582b3685e0a17f7 Mon Sep 17 00:00:00 2001 From: Connor Sullivan Date: Sun, 4 Aug 2024 00:40:39 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20before=20ad?= =?UTF-8?q?ding=20subcommands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors the existing implementation in advance of adding additional subcommands. dist/main.js: 3.3kb (+3%) --- CONTRIBUTING.md | 2 +- README.md | 2 +- fixtures/fixtures-root.ts | 1 + src/cli/command-utils.ts | 76 +++++++++++++ src/{ => cli}/config.ts | 2 +- .../generate-logic.test.ts} | 9 +- src/{logic.ts => commands/generate-logic.ts} | 2 +- src/commands/generate.ts | 43 +++++++ src/main.ts | 106 ++---------------- src/version.ts | 2 + 10 files changed, 139 insertions(+), 106 deletions(-) create mode 100644 fixtures/fixtures-root.ts create mode 100644 src/cli/command-utils.ts rename src/{ => cli}/config.ts (98%) rename src/{logic.test.ts => commands/generate-logic.test.ts} (86%) rename src/{logic.ts => commands/generate-logic.ts} (98%) create mode 100644 src/commands/generate.ts create mode 100644 src/version.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06df752..4d9d09f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SH The [src](./src) directory contains the main and test sources. - [main.ts](./src/main.ts) represents the entry point (the CLI tool). -- [logic.ts](./src/logic.ts) represents the unit-tested logic. +- [logic.ts](src/commands/logic.ts) represents the unit-tested logic. The [fixtures](fixtures) directory contains files for data-file-driven unit tests. diff --git a/README.md b/README.md index 51e7c2b..c45ee34 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Extensionless "rc" files can have JSON or YAML format. Under the hood, `css-typed` uses [lilconfig] to load configuration files. It supports YAML files via [js-yaml]. -See [src/config.ts](src/config.ts) for the implementation. +See [src/config.ts](src/cli/config.ts) for the implementation. diff --git a/fixtures/fixtures-root.ts b/fixtures/fixtures-root.ts new file mode 100644 index 0000000..47b21fb --- /dev/null +++ b/fixtures/fixtures-root.ts @@ -0,0 +1 @@ +export const fixturesRoot = import.meta.dirname; diff --git a/src/cli/command-utils.ts b/src/cli/command-utils.ts new file mode 100644 index 0000000..8989da4 --- /dev/null +++ b/src/cli/command-utils.ts @@ -0,0 +1,76 @@ +import { Command, Option } from "@commander-js/extra-typings"; +import { glob } from "glob"; + +import type { Options } from "../options.ts"; +import { localsConventionChoices } from "../options.ts"; +import { version } from "../version.ts"; +import { loadFileConfig } from "./config.ts"; + +// eslint-disable-next-line quotes -- Module must be quotes +declare module "@commander-js/extra-typings" { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- Extending class + interface Command { + cssTypedAction: typeof cssTypedAction; + } +} + +/** Creates css-typed command with global options. */ +export function createCssTypedCommand() { + const command = new Command() + .argument(`[pattern]`, `Glob path for CSS files to target.`) + .option(`-c, --config `, `Custom path to the configuration file.`) + .addOption( + new Option( + `--localsConvention `, + `Style of exported classnames. See https://github.com/connorjs/css-typed/tree/v${version}#localsConvention`, + ) + .choices(localsConventionChoices) + .default(`dashesOnly` as const), + ) + .option( + `-o, --outdir `, + `Root directory for generated CSS declaration files.`, + ); + command.cssTypedAction = cssTypedAction.bind(command); + return command; +} + +/** Standardizes global option handling and simplifies action interface. */ +function cssTypedAction( + this: Command<[string | undefined], Partial & { config?: string }>, + fileHandler: (files: string[], options: Options) => Promise, +) { + return this.action( + async (cliPattern, { config: cliConfigPath, ...cliOptions }, program) => { + // Load file configuration first + const configResult = await loadFileConfig(cliConfigPath); + if (configResult?.filepath) { + // We loaded the file + console.debug( + `[debug] Reading configuration from ${configResult.filepath}.`, + ); + } else if (cliConfigPath) { + // We did not load the file, but we expected to with `-c/--config`, so error + return program.error(`[error] Failed to parse ${cliConfigPath}.`); + } + + // Remove pattern argument from file config, if present. + const { pattern: filePattern, ...fileConfig } = + configResult?.config ?? {}; + + // Resolve options from file config and CLI. CLI overrides file config. + const options: Options = { ...fileConfig, ...cliOptions }; + + // Pattern is required. CLI overrides file config. + const pattern = cliPattern ?? filePattern; + if (!pattern) { + // Match commander error message + return program.error(`[error] Missing required argument 'pattern'`); + } + + // Find the files and delegate them to the callback + const files = await glob(pattern); + return fileHandler(files, options); + }, + ); +} diff --git a/src/config.ts b/src/cli/config.ts similarity index 98% rename from src/config.ts rename to src/cli/config.ts index e59cc28..bd31347 100644 --- a/src/config.ts +++ b/src/cli/config.ts @@ -3,7 +3,7 @@ import type { LilconfigResult, Loaders } from "lilconfig"; import { lilconfig } from "lilconfig"; import type { OverrideProperties } from "type-fest"; -import type { Options } from "./options.ts"; +import type { Options } from "../options.ts"; const name = `css-typed`; const rcAlt = `csstyped`; diff --git a/src/logic.test.ts b/src/commands/generate-logic.test.ts similarity index 86% rename from src/logic.test.ts rename to src/commands/generate-logic.test.ts index be1ab81..c588547 100644 --- a/src/logic.test.ts +++ b/src/commands/generate-logic.test.ts @@ -3,9 +3,10 @@ import * as process from "node:process"; import { describe, expect, it } from "vitest"; -import { dtsPath, generateDeclaration } from "./logic.js"; -import type { Options } from "./options.ts"; -import { localsConventionChoices } from "./options.ts"; +import { fixturesRoot } from "../../fixtures/fixtures-root.ts"; +import type { Options } from "../options.ts"; +import { localsConventionChoices } from "../options.ts"; +import { dtsPath, generateDeclaration } from "./generate-logic.ts"; describe(`css-typed`, () => { it(`should not generate an empty declaration file [#9]`, async () => { @@ -54,5 +55,5 @@ describe(`css-typed`, () => { }); function fixtureFile(filename: string) { - return path.join(import.meta.dirname, `..`, `fixtures`, filename); + return path.join(fixturesRoot, filename); } diff --git a/src/logic.ts b/src/commands/generate-logic.ts similarity index 98% rename from src/logic.ts rename to src/commands/generate-logic.ts index 8ae360e..cc426a7 100644 --- a/src/logic.ts +++ b/src/commands/generate-logic.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { parse as parseCss, walk } from "css-tree"; import camelCase from "lodash.camelcase"; -import type { LocalsConvention, Options } from "./options.ts"; +import type { LocalsConvention, Options } from "../options.ts"; /** * Generates TypeScript declaration file for the stylesheet file at the given diff --git a/src/commands/generate.ts b/src/commands/generate.ts new file mode 100644 index 0000000..2cc6569 --- /dev/null +++ b/src/commands/generate.ts @@ -0,0 +1,43 @@ +import { existsSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; + +import type { Options } from "../options.ts"; +import { dtsPath, generateDeclaration } from "./generate-logic.ts"; + +export async function generate(files: string[], options: Options) { + const time = new Date().toISOString(); + await Promise.all( + files.map((file) => + generateDeclaration(file, time, options).then((ts) => + writeDeclarationFile(file, options.outdir, ts), + ), + ), + ); +} + +/** + * Writes the TypeScript declaration content to file. Handles the output path. + * + * @param file - Path to the original stylesheet file. NOT the path to write. + * @param outdir - Output directory to which to write. + * @param ts - The TypeScript declaration content to write. + * @returns Empty promise indicating when writing has completed. + */ +async function writeDeclarationFile( + file: string, + outdir: string | undefined, + ts: string | undefined, +) { + if (!ts) { + return undefined; + } + + const [directoryToWrite, fileToWrite] = dtsPath(file, outdir); + if (!existsSync(directoryToWrite)) { + await mkdir(directoryToWrite, { recursive: true }); + } + + const pathToWrite = path.join(directoryToWrite, fileToWrite); + await writeFile(pathToWrite, ts, { encoding: `utf8` }); +} diff --git a/src/main.ts b/src/main.ts index 81b7069..7500a94 100755 --- a/src/main.ts +++ b/src/main.ts @@ -1,104 +1,14 @@ #!/usr/bin/env node -import { existsSync } from "node:fs"; -import { mkdir, writeFile } from "node:fs/promises"; -import path from "node:path"; +import { createCssTypedCommand } from "./cli/command-utils.ts"; +import { generate } from "./commands/generate.ts"; +import { version } from "./version.ts"; -import { Command, Option } from "@commander-js/extra-typings"; -import { glob } from "glob"; - -import { loadFileConfig } from "./config.ts"; -import { dtsPath, generateDeclaration } from "./logic.js"; -import type { Options } from "./options.ts"; -import { localsConventionChoices } from "./options.ts"; - -declare let VERSION: string; // Defined by esbuild -const version = VERSION; - -await new Command() +await createCssTypedCommand() .name(`css-typed`) - .description(`TypeScript declaration generator for CSS files.`) - .version(version) - .argument(`[pattern]`, `Glob path for CSS files to target.`) - .option(`-c, --config `, `Custom path to the configuration file.`) - .addOption( - new Option( - `--localsConvention `, - `Style of exported classnames. See https://github.com/connorjs/css-typed/tree/v${version}#localsConvention`, - ) - .choices(localsConventionChoices) - .default(`dashesOnly` as const), + .description( + `TypeScript declaration generator for CSS files (and other stylesheets).`, ) - .option( - `-o, --outdir `, - `Root directory for generated CSS declaration files.`, - ) - .action(async function ( - cliPattern, - { config: cliConfigPath, ...cliOptions }, - program, - ) { - // Load file configuration first - const configResult = await loadFileConfig(cliConfigPath); - if (configResult?.filepath) { - // We loaded the file - console.debug( - `[debug] Reading configuration from ${configResult.filepath}.`, - ); - } else if (cliConfigPath) { - // We did not load the file, but we expected to with `-c/--config`, so error - return program.error(`[error] Failed to parse ${cliConfigPath}.`); - } - - // Remove pattern argument from file config, if present. - const { pattern: filePattern, ...fileConfig } = configResult?.config ?? {}; - - // Resolve options from file config and CLI. CLI overrides file config. - const options: Options = { ...fileConfig, ...cliOptions }; - - // Pattern is required. CLI overrides file config. - const pattern = cliPattern ?? filePattern; - if (!pattern) { - // Match commander error message - return program.error(`[error] Missing required argument 'pattern'`); - } - - // Find the files and process each. - const files = await glob(pattern); - - const time = new Date().toISOString(); - await Promise.all( - files.map((file) => - generateDeclaration(file, time, options).then((ts) => - writeDeclarationFile(file, options.outdir, ts), - ), - ), - ); - }) + .version(version) + .cssTypedAction(generate) .parseAsync(); - -/** - * Writes the TypeScript declaration content to file. Handles the output path. - * - * @param file - Path to the original stylesheet file. NOT the path to write. - * @param outdir - Output directory to which to write. - * @param ts - The TypeScript declaration content to write. - * @returns Empty promise indicating when writing has completed. - */ -async function writeDeclarationFile( - file: string, - outdir: string | undefined, - ts: string | undefined, -) { - if (!ts) { - return undefined; - } - - const [directoryToWrite, fileToWrite] = dtsPath(file, outdir); - if (!existsSync(directoryToWrite)) { - await mkdir(directoryToWrite, { recursive: true }); - } - - const pathToWrite = path.join(directoryToWrite, fileToWrite); - await writeFile(pathToWrite, ts, { encoding: `utf8` }); -} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..8952efa --- /dev/null +++ b/src/version.ts @@ -0,0 +1,2 @@ +declare let VERSION: string; // Defined by esbuild +export const version = VERSION; From 4b40dd415180f0cd46ef34870cb34d7fa74e4e3d Mon Sep 17 00:00:00 2001 From: Connor Sullivan Date: Sun, 4 Aug 2024 00:59:52 -0400 Subject: [PATCH 2/2] doc improvements --- CONTRIBUTING.md | 3 ++- README.md | 2 +- src/commands/generate.ts | 16 ++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d9d09f..87f6ca8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,8 @@ The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SH The [src](./src) directory contains the main and test sources. - [main.ts](./src/main.ts) represents the entry point (the CLI tool). -- [logic.ts](src/commands/logic.ts) represents the unit-tested logic. +- [cli](./src/cli) contains CLI-related code that is not unit tested. +- [commands](src/commands) represents the unit-tested commands and logic. The [fixtures](fixtures) directory contains files for data-file-driven unit tests. diff --git a/README.md b/README.md index c45ee34..0aab567 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Extensionless "rc" files can have JSON or YAML format. Under the hood, `css-typed` uses [lilconfig] to load configuration files. It supports YAML files via [js-yaml]. -See [src/config.ts](src/cli/config.ts) for the implementation. +See [config.ts](src/cli/config.ts) for the implementation. diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 2cc6569..e5510f4 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -5,6 +5,13 @@ import path from "node:path"; import type { Options } from "../options.ts"; import { dtsPath, generateDeclaration } from "./generate-logic.ts"; +/** + * Generates the TypeScript declaration files and writes them to file. + * + * @param files - Paths to the original stylesheet files. + * @param options - Options object. + * @returns Empty promise indicating when writing has completed. + */ export async function generate(files: string[], options: Options) { const time = new Date().toISOString(); await Promise.all( @@ -16,14 +23,7 @@ export async function generate(files: string[], options: Options) { ); } -/** - * Writes the TypeScript declaration content to file. Handles the output path. - * - * @param file - Path to the original stylesheet file. NOT the path to write. - * @param outdir - Output directory to which to write. - * @param ts - The TypeScript declaration content to write. - * @returns Empty promise indicating when writing has completed. - */ +/** Writes the TypeScript declaration content to file. Handles the output path. */ async function writeDeclarationFile( file: string, outdir: string | undefined,