From b65f889c8c2ad92be7565ed2110fe9809588b74c Mon Sep 17 00:00:00 2001 From: Connor Sullivan Date: Thu, 23 Mar 2023 22:43:46 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Initial=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Releases the initial (`0.1.0`) version. Provides a basic implementation that emits `.d.css.ts` files with named exports for the class names in the files matching the given glob. Updates README with guidance. Adds `glob` and `css-tree`. --- README.md | 99 ++++++++++++++++++++++++++++++++++++++- main.js | 56 ++++++++++++++++++++++ package-lock.json | 116 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 13 +++++- 4 files changed, 279 insertions(+), 5 deletions(-) create mode 100755 main.js diff --git a/README.md b/README.md index d0c65fd..ed4b50d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,90 @@ Basic TypeScript declaration generator for CSS files. +## Install + +Install the CLI as a dev dependency. + +```shell +npm install --save-dev css-typed +``` + +## Usage + +Run `css-typed` and pass it a glob targeting your CSS files. + +```shell +npx css-typed 'src/**/*.css' +``` + +This will generate `.d.css.ts` files next to the original source files. + +> Note: A CSS module file with the name `foo.module.css` will +> emit `foo.module.d.css.ts` + +## Configuration + +Configure TypeScript to allow arbitrary extensions (TS 5+). + +```json +{ + "compilerOptions": { + "allowArbitraryExtensions": true + } +} +``` + +Add `*.d.css.ts` to your `.gitignore` if appropriate. + +```shell +echo '*.d.css.ts' >> .gitignore +``` + +## Recipes + +### Run script + +To run it as part of your build, you will likely include it as a run script, +maybe as `codegen` or `pretsc`. + +```json +{ + "scripts": { + "codegen": "css-typed 'src/**/*.css'", + "pretsc": "css-typed 'src/**/*.css'", + "tsc": "tsc" + } +} +``` + +### Watch + +The CLI does not have built-in watch support. Feel free to [nodemon] or similar. + +```json +{ + "scripts": { + "codegen": "css-typed 'src/**/*.css'", + "codegen:watch": "nodemon -x 'npm run codegen' -w src -e css" + } +} +``` + +[nodemon]: https://www.npmjs.com/package/nodemon + +## Details + +This (very basic) implementation uses [glob] for file matching and [css-tree] +for CSS parsing. It extracts CSS classes (`ClassSelector` in CSS Tree’s AST) and +exports them as `string` constants (named exports). + +I chose CSS Tree after a brief search because it had a nice API, good +documentation, and supported CSS nesting (a requirement for my original use +case). + +[glob]: https://www.npmjs.com/package/glob +[css-tree]: https://www.npmjs.com/package/css-tree + ## Motivation [typescript-plugin-css-modules] provides a great IDE experience, but cannot @@ -24,10 +108,21 @@ appears [abandoned][174]. Therefore, I wrote my own (very basic) implementation. - [typescript-plugin-css-modules]: https://www.npmjs.com/package/typescript-plugin-css-modules [typed-css-modules]: https://www.npmjs.com/package/typed-css-modules [typed-scss-modules]: https://www.npmjs.com/package/typed-scss-modules [css-modules-loader-core]: https://www.npmjs.com/package/css-modules-loader-core [174]: https://github.com/css-modules/css-modules-loader-core/issues/174 - + +## Future + +This (very basic) implementation suited my immediate needs, but I see some +improvements we could make. _All naming subject to bike shedding._ + +- `ext`: Traditional (pre TS 5) extension naming with `*.css.d.ts` +- `ignore`: Ignore support +- `format`: Class name formatting + - (Related) Gracefully handle invalid names (example: kebab case) +- `outDir`: Publish to a directory instead of next to the sources +- `watch`: First-class watch mode +- General CLI/UX improvements diff --git a/main.js b/main.js new file mode 100755 index 0000000..e923d5b --- /dev/null +++ b/main.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +import { existsSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { join, parse as parsePath } from "node:path"; + +import { parse as parseCss, walk } from "css-tree"; +import { glob } from "glob"; + +await main(process.argv[2]); + +async function main(pattern) { + if (!pattern) { + console.error(`Expected glob pattern`); + process.exit(2); + } + + const files = await glob(pattern); + + const time = new Date().toISOString(); + const results = await Promise.all( + files.map((file) => generateDeclaration(file, time)), + ); + + const errors = results.filter(Boolean); + if (errors.length > 0) { + console.error(`Errors encountered`, errors); + process.exit(3); + } + + process.exit(0); +} + +async function generateDeclaration(path, time) { + // Handle case where the file got deleted by the time we got here + if (!existsSync(path)) return; + + const css = await readFile(path, `utf8`); + + let ts = `// Generated from ${path} by css-typed at ${time}\n\n`; + + const ast = parseCss(css, { filename: path }); + walk(ast, (node) => { + if (node.type === `ClassSelector`) { + ts += `export const ${node.name}: string;\n`; + } + }); + + await writeFile(dtsPath(path), ts, `utf8`); + return undefined; +} + +function dtsPath(path) { + const { dir, name, ext } = parsePath(path); + return join(dir, `${name}.d${ext}.ts`); +} diff --git a/package-lock.json b/package-lock.json index 38af76b..80b5490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,20 @@ { "name": "css-typed", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "css-typed", - "version": "0.0.1", + "version": "0.1.0", "license": "MIT", + "dependencies": { + "css-tree": "^2.3.1", + "glob": "^9.3.2" + }, + "bin": { + "css-typed": "main.js" + }, "devDependencies": { "husky": "^8.0.3", "is-ci": "^3.0.1", @@ -88,6 +95,19 @@ "node": ">=8" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -199,6 +219,18 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -263,6 +295,11 @@ "node": ">=8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -275,6 +312,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.2.tgz", + "integrity": "sha512-BTv/JhKXFEHsErMte/AnfiSv8yYOLLiyH2lTg8vn02O21zWFgHPTfxtgn1QRe7NRgggUhC8hacR2Re94svHqeA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^7.4.1", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -574,6 +628,19 @@ "node": ">=8" } }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -605,6 +672,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.3.tgz", + "integrity": "sha512-5UB4yYusDtkRPbRiy1cqZ1IpGNcJCGlEMG17RKzPddpyiPKoCdwohbED8g4QXT0ewCt8LTkQXuljsUfQ3FKM4A==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", + "integrity": "sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -695,6 +784,21 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.3.tgz", + "integrity": "sha512-RAmB+n30SlN+HnNx6EbcpoDy9nwdpcGPnEKrJnu6GZoDWBdIjo1UQMVtW2ybtC7LC2oKLcMq8y5g8WnKLiod9g==", + "dependencies": { + "lru-cache": "^7.14.1", + "minipass": "^4.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -841,6 +945,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", diff --git a/package.json b/package.json index 2fc9b4d..5ad0519 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "css-typed", - "version": "0.0.1", + "version": "0.1.0", "description": "Basic TypeScript declaration generator for CSS files", "keywords": [ "CSS", @@ -23,6 +23,10 @@ "type": "git", "url": "git+https://github.com/connorjs/css-typed.git" }, + "type": "module", + "bin": { + "css-typed": "main.js" + }, "scripts": { "format": "prettier -l '**/*.{json,md}' --ignore-path .gitignore", "prepare": "is-ci || husky install", @@ -31,10 +35,17 @@ "lint-staged": { "*.{json,md}": "prettier -w" }, + "dependencies": { + "css-tree": "^2.3.1", + "glob": "^9.3.2" + }, "devDependencies": { "husky": "^8.0.3", "is-ci": "^3.0.1", "lint-staged": "^13.2.0", "prettier": "^2.8.6" + }, + "engines": { + "node": ">=14" } }