Skip to content

Commit

Permalink
✨ Initial release
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
connorjs committed Mar 24, 2023
1 parent 947f9ba commit b65f889
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 5 deletions.
99 changes: 97 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,10 +108,21 @@ appears [abandoned][174].

Therefore, I wrote my own (very basic) implementation.

<!-- prettier-ignore-start -->
[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
<!-- prettier-ignore-end -->

## 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
56 changes: 56 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
@@ -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`);
}
116 changes: 114 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -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"
}
}

0 comments on commit b65f889

Please sign in to comment.