From 6a0640a25ca7a3b4ab4df532eba73cea472bff95 Mon Sep 17 00:00:00 2001 From: Connor Sullivan Date: Sat, 23 Dec 2023 15:48:21 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Handle=20kebab-case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the `--dashes` option (subject to change for v1.0) to supports emitting `camelCase` class names in place of `kebab-case` ones. Changes the default to drop invalid (e.g. `kebab-case` names) from the generated TypeScript. Note: Although the generated default has changed, releasing under `0.2` (no breaking change) because the existing emitted TS was invalid. Adds `css` to Prettier config. --- README.md | 25 ++++++++++++++++ lint-staged.config.cjs | 2 +- package.json | 4 +-- src/fixtures/foo.css | 8 ++--- src/fixtures/foo.module.css | 8 ++--- .../kebab-case/kebab-case-dashes.d.css.ts | 7 +++++ .../kebab-case/kebab-case-default.d.css.ts | 4 +++ src/fixtures/kebab-case/kebab-case.css | 22 ++++++++++++++ src/logic.js | 28 ++++++++++++++--- src/logic.test.js | 30 ++++++++++++++++--- src/main.js | 8 +++-- 11 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 src/fixtures/kebab-case/kebab-case-dashes.d.css.ts create mode 100644 src/fixtures/kebab-case/kebab-case-default.d.css.ts create mode 100644 src/fixtures/kebab-case/kebab-case.css diff --git a/README.md b/README.md index bfb1dda..08dfbfa 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,31 @@ Add `*.d.css.ts` to your `.gitignore` if appropriate. echo '*.d.css.ts' >> .gitignore ``` +## Options + +The following table lists the options `css-typed` supports. +Prior to the `1.0` release, these may change often. + +| CLI option | Description | +| :--------: | :---------------------------------------- | +| `--dashes` | Specifies the convention used for locals. | + +### Dashes + +_Inspired by [postcss’ localsConvention](https://github.com/madyankin/postcss-modules/tree/master#localsconvention). +Prior to `v1.0`, this option will evolve to more closely match the `localsConvention` option._ + +The `--dashes` option changes the style of exported classnames, the exports in your TS. + +By default, `css-typed` will emit class names as-is if the name represents a valid JS/TS identifier. +_Note: The logic for “valid” only checks hyphens (dashes, `-`) as of `v0.2.2`._ + +When passed `dashes`, it will transform `kebab-case` classes (dashed names) to `camelCase`. +For example, `my-class` becomes `myClass`. + +Use `--dashes` when your bundler or build system supports that transformation. +For example, Vite and Gatsby support this. + ## Recipes ### Run script diff --git a/lint-staged.config.cjs b/lint-staged.config.cjs index ea372eb..ac3d1f7 100644 --- a/lint-staged.config.cjs +++ b/lint-staged.config.cjs @@ -1,4 +1,4 @@ module.exports = { "*.{cjs,js,json}": [`prettier -w`, `eslint -f pretty --fix`], - "*.{md,yaml,yml}": `prettier -w`, + "*.{css,md,yaml,yml}": `prettier -w`, }; diff --git a/package.json b/package.json index 72d9bc1..a8db3df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "css-typed", - "version": "0.2.1", + "version": "0.2.2", "description": "Basic TypeScript declaration generator for CSS files", "keywords": [ "CSS", @@ -35,7 +35,7 @@ "scripts": { "build": "rm -rf dist && mkdir -p dist && cp src/main.js src/logic.js dist", "ci-build": "npm-run-all -l -p format lint test build", - "format": "prettier -l '**/*.{cjs,js,json,md,yaml,yml}' --ignore-path .gitignore", + "format": "prettier -l '**/*.{cjs,css,js,json,md,yaml,yml}' --ignore-path .gitignore", "lint": "eslint -f pretty .", "prepare": "is-ci || husky install", "prepublishOnly": "npm run ci-build", diff --git a/src/fixtures/foo.css b/src/fixtures/foo.css index 62425f6..ed1a226 100644 --- a/src/fixtures/foo.css +++ b/src/fixtures/foo.css @@ -1,15 +1,15 @@ .foo { - color: red; + color: red; } .bar { - color: blue; + color: blue; } .foo .baz { - color: green; + color: green; } .fooBarBaz { - color: orange; + color: orange; } diff --git a/src/fixtures/foo.module.css b/src/fixtures/foo.module.css index 62425f6..ed1a226 100644 --- a/src/fixtures/foo.module.css +++ b/src/fixtures/foo.module.css @@ -1,15 +1,15 @@ .foo { - color: red; + color: red; } .bar { - color: blue; + color: blue; } .foo .baz { - color: green; + color: green; } .fooBarBaz { - color: orange; + color: orange; } diff --git a/src/fixtures/kebab-case/kebab-case-dashes.d.css.ts b/src/fixtures/kebab-case/kebab-case-dashes.d.css.ts new file mode 100644 index 0000000..3a15f60 --- /dev/null +++ b/src/fixtures/kebab-case/kebab-case-dashes.d.css.ts @@ -0,0 +1,7 @@ +// Generated from `src/fixtures/kebab-case/kebab-case.css` by css-typed at $TIME + +export const container: string; +export const heading: string; +export const navLinks: string; +export const navLinkItem: string; +export const navLinkText: string; diff --git a/src/fixtures/kebab-case/kebab-case-default.d.css.ts b/src/fixtures/kebab-case/kebab-case-default.d.css.ts new file mode 100644 index 0000000..117990c --- /dev/null +++ b/src/fixtures/kebab-case/kebab-case-default.d.css.ts @@ -0,0 +1,4 @@ +// Generated from `src/fixtures/kebab-case/kebab-case.css` by css-typed at $TIME + +export const container: string; +export const heading: string; diff --git a/src/fixtures/kebab-case/kebab-case.css b/src/fixtures/kebab-case/kebab-case.css new file mode 100644 index 0000000..e3145a5 --- /dev/null +++ b/src/fixtures/kebab-case/kebab-case.css @@ -0,0 +1,22 @@ +/* Originally from https://github.com/connorjs/css-typed/pull/1 */ + +.container { + margin: auto; + max-width: 500px; + font-family: sans-serif; +} + +.heading { + color: rebeccapurple; +} +.nav-links { + display: flex; + list-style: none; + padding-left: 0; +} +.nav-link-item { + padding-right: 2rem; +} +.nav-link-text { + color: black; +} diff --git a/src/logic.js b/src/logic.js index 0b02d16..8fd987e 100644 --- a/src/logic.js +++ b/src/logic.js @@ -6,7 +6,11 @@ import { parse as parseCss, walk } from "css-tree"; /* globals process -- Node/CLI tool */ -export async function generateDeclaration(path, time) { +export async function generateDeclaration( + /*string*/ path, + /*string*/ time, + /*{localsConvention?: "dashes"}*/ options, +) { // Handle case where the file got deleted by the time we got here if (!existsSync(path)) return; @@ -23,15 +27,31 @@ export async function generateDeclaration(path, time) { // Skip duplicate names if (exportedNames.has(node.name)) return; - ts += `export const ${node.name}: string;\n`; - exportedNames.add(node.name); + // Skip dashed names (kebab-case), unless `localsConvention` is `dashes`. + const nameHasDashes = hasDashes(node.name); + if (nameHasDashes && options.localsConvention !== `dashes`) return; + + const nodeName = nameHasDashes ? dashesCamelCase(node.name) : node.name; + + ts += `export const ${nodeName}: string;\n`; + exportedNames.add(nodeName); } }); return ts; } -export function dtsPath(path) { +function hasDashes(/*string*/ s) { + return s.includes(`-`); +} + +// Modifies postcss-modules `dashesCamelCase` function to use `replaceAll` given +// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-string-replace-all.md +function dashesCamelCase(/*string*/ s) { + return s.replaceAll(/-+(\w)/g, (_, firstLetter) => firstLetter.toUpperCase()); +} + +export function dtsPath(/*string*/ path) { const { dir, name, ext } = parsePath(path); return join(dir, `${name}.d${ext}.ts`); } diff --git a/src/logic.test.js b/src/logic.test.js index 31e16ba..3d37e6a 100644 --- a/src/logic.test.js +++ b/src/logic.test.js @@ -9,11 +9,33 @@ import { dtsPath, generateDeclaration } from "./logic.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); describe(`css-typed`, () => { - describe.each([`foo.css`, `foo.module.css`])(`%s`, (filename) => { + describe.each([ + [`foo.css`, `foo.d.css.ts`, {}], + [`foo.module.css`, `foo.module.d.css.ts`, {}], + [`kebab-case/kebab-case.css`, `kebab-case/kebab-case-default.d.css.ts`, {}], + [ + `kebab-case/kebab-case.css`, + `kebab-case/kebab-case-dashes.d.css.ts`, + { localsConvention: `dashes` }, + ], + ])(`%s → %s`, (inputFilename, outputFilename, options) => { it(`should match expected output`, async () => { - const path = join(__dirname, `fixtures`, filename); - const expected = readFileSync(dtsPath(path), { encoding: `utf8` }); - expect(await generateDeclaration(path, `$TIME`)).toStrictEqual(expected); + const inputPath = join(__dirname, `fixtures`, inputFilename); + const outputPath = join(__dirname, `fixtures`, outputFilename); + + const expected = readFileSync(outputPath, { encoding: `utf8` }); + + const generated = await generateDeclaration(inputPath, `$TIME`, options); + expect(generated).toStrictEqual(expected); + }); + }); + + describe(`dtsPath`, () => { + it.each([ + [`foo.css`, `foo.d.css.ts`], + [`foo.module.css`, `foo.module.d.css.ts`], + ])(`%s should create file %s`, (input, expected) => { + expect(dtsPath(input)).toStrictEqual(expected); }); }); }); diff --git a/src/main.js b/src/main.js index e933054..43dac66 100755 --- a/src/main.js +++ b/src/main.js @@ -7,20 +7,22 @@ import { glob } from "glob"; import { dtsPath, generateDeclaration } from "./logic.js"; /* globals process -- Node/CLI tool */ -await main(process.argv[2]); +await main(process.argv[2], process.argv[3] === `--dashes`); +// See https://github.com/connorjs/css-typed/issues/5 for "proper" CLI arg handling -async function main(pattern) { +async function main(pattern, dashesEnabled) { if (!pattern) { console.error(`Expected glob pattern`); process.exit(2); } + const options = dashesEnabled ? { localsConvention: `dashes` } : {}; const files = await glob(pattern); const time = new Date().toISOString(); const results = await Promise.all( files.map((file) => - generateDeclaration(file, time).then((ts) => + generateDeclaration(file, time, options).then((ts) => writeDeclarationFile(file, ts), ), ), From 4387ad9e63533c102de25c25424691a42adf90d5 Mon Sep 17 00:00:00 2001 From: Connor Sullivan Date: Mon, 25 Dec 2023 10:05:26 -0500 Subject: [PATCH 2/2] Apply formatting after merge --- src/fixtures/no-declaration-file.css | 4 ++-- src/logic.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fixtures/no-declaration-file.css b/src/fixtures/no-declaration-file.css index 07ddce3..e68ef78 100644 --- a/src/fixtures/no-declaration-file.css +++ b/src/fixtures/no-declaration-file.css @@ -1,9 +1,9 @@ html { - box-sizing: border-box; + box-sizing: border-box; } *, *:before, *:after { - box-sizing: inherit; + box-sizing: inherit; } diff --git a/src/logic.test.js b/src/logic.test.js index bc5786b..dc2371f 100644 --- a/src/logic.test.js +++ b/src/logic.test.js @@ -13,7 +13,7 @@ describe(`css-typed`, () => { const path = fixtureFile(`no-declaration-file.css`); expect(await generateDeclaration(path, `$TIME`)).toBeUndefined(); }); - + describe.each([ [`foo.css`, `foo.d.css.ts`, {}], [`foo.module.css`, `foo.module.d.css.ts`, {}],