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/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.js b/src/logic.js index 50499aa..1d4546f 100644 --- a/src/logic.js +++ b/src/logic.js @@ -12,10 +12,11 @@ import { parse as parseCss, walk } from "css-tree"; * * @param path {string} - Path to stylesheet file. * @param time {string} - Timestamp string to include in generated comment. + * @param options {{localsConvention?: "dashes"}} - Options object. * @returns {Promise} TypeScript declaration file content or * `undefined` if no declarations to write. */ -export async function generateDeclaration(path, time) { +export async function generateDeclaration(path, time, options) { // Handle case where the file got deleted by the time we got here if (!existsSync(path)) return undefined; @@ -32,8 +33,14 @@ 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); } }); @@ -41,7 +48,17 @@ export async function generateDeclaration(path, time) { return exportedNames.size === 0 ? undefined : 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 849f4b4..dc2371f 100644 --- a/src/logic.test.js +++ b/src/logic.test.js @@ -14,11 +14,33 @@ describe(`css-typed`, () => { expect(await generateDeclaration(path, `$TIME`)).toBeUndefined(); }); - 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 = fixtureFile(filename); - const expected = readFileSync(dtsPath(path), { encoding: `utf8` }); - expect(await generateDeclaration(path, `$TIME`)).toStrictEqual(expected); + const inputPath = fixtureFile(inputFilename); + const outputPath = fixtureFile(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 363f3d8..0a298fb 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), ), ),