Happy CSS Modules
Typed, definition jumpable CSS Modules.
Moreover, easy!
demo.mov
Features
- โ
Strict type checking
- Generate
.d.ts
of CSS Modules for type checking
- Generate
- ๐ Definition jumps
- Clicking on a property on
.jsx
/.tsx
will jump to the source of the definition on.module.css
. - This is accomplished by generating
.d.ts.map
(a.k.a. Declaration Map).
- Clicking on a property on
- ๐ค High compatibility with the ecosystem
- Support for Postcss/Sass/Less
- Implement webpack-compatible resolving algorithms
- Also supports
resolve.alias
- ๐ฐ Easy to use
- No configuration file, some simple CLI options
Installation
$ npm i -D happy-css-modules
Usage
In the simple case, everything goes well with the following!
$ hcm 'src/**/*.module.{css,scss,less}'
If you want to customize the behavior, see --help
.
$ hcm --help
Generate .d.ts and .d.ts.map for CSS modules.
hcm [options] <glob>
Options:
-w, --watch Watch input directory's css files or pattern [boolean] [default: false]
--localsConvention Style of exported class names. [choices: "camelCase", "camelCaseOnly", "dashes", "dashesOnly"]
--declarationMap Create sourcemaps for d.ts files [boolean] [default: true]
--sassLoadPaths The option compatible with sass's `--load-path`. [array]
--lessIncludePaths The option compatible with less's `--include-path`. [array]
--webpackResolveAlias The option compatible with webpack's `resolve.alias`. [string]
--postcssConfig The option compatible with postcss's `--config`. [string]
--arbitraryExtensions Generate `.d.css.ts` instead of `.css.d.ts`. [boolean] [default: true]
--cache Only generate .d.ts and .d.ts.map for changed files. [boolean] [default: true]
--cacheStrategy Strategy for the cache to use for detecting changed files.[choices: "content", "metadata"] [default: "content"]
--logLevel What level of logs to report. [choices: "debug", "info", "silent"] [default: "info"]
-h, --help Show help [boolean]
-v, --version Show version number [boolean]
Examples:
hcm 'src/**/*.module.css' Generate .d.ts and .d.ts.map.
hcm 'src/**/*.module.{css,scss,less}' Also generate files for sass and less.
hcm 'src/**/*.module.css' --watch Watch for changes and generate .d.ts and .d.ts.map.
hcm 'src/**/*.module.css' --declarationMap=false Generate .d.ts only.
hcm 'src/**/*.module.css' --sassLoadPaths=src/style Run with sass's `--load-path`.
hcm 'src/**/*.module.css' --lessIncludePaths=src/style Run with less's `--include-path`.
hcm 'src/**/*.module.css' --webpackResolveAlias='{"@": "src"}' Run with webpack's `resolve.alias`.
hcm 'src/**/*.module.css' --cache=false Disable cache.
How docs definition jumps work?
In addition to .module.css.d.ts
, happy-css-modules also generates a .module.css.d.ts.map
file (a.k.a. Declaration Map). This file is a Source Map that contains code mapping information from generated (.module.css.d.ts
) to source (.module.css
).
When tsserver (TypeScript Language Server for VSCode) tries to jump to the code on .module.css.d.ts
, it restores the original location from this Source Map and redirects to the code on .module.css
. happy-css-modules uses this mechanism to realize definition jump.
The case of multiple definitions is a bit more complicated. This is because the Source Map specification does not allow for a 1:N mapping of the generated:original locations. Therefore, happy-css-modules define multiple definitions of the same property type and map each property to a different location in .module.css
.
Node.js API (Experimental)
Warning This feature is experimental and may change significantly. The API is not stable and may have breaking changes even in minor or patch version updates.
happy-css-modules
provides Node.js API for programmatically generating .d.ts and .d.ts.map.
See src/index.ts for available API.
hcm
commands
Example: Custom You can create your own customized hcm
commands. We also provide a parseArgv
utility that parses process.argv
and extracts options.
#!/usr/bin/env ts-node
// scripts/hcm.ts
import { run, parseArgv } from 'happy-css-modules';
// Write your code here...
run({
// Inherit default CLI options (e.g. --watch).
...parseArgv(process.argv),
// Add custom CLI options.
cwd: __dirname,
}).catch((e) => {
console.error(e);
process.exit(1);
});
Example: Custom transformer
With the transformer
option, you can use AltCSS, which is not supported by happy-css-modules
.
#!/usr/bin/env ts-node
import { run, parseArgv, createDefaultTransformer, type Transformer } from 'happy-css-modules';
import sass from 'sass';
import { promisify } from 'util';
const defaultTransformer = createDefaultTransformer();
const render = promisify(sass.render);
// The custom transformer supporting sass indented syntax
const transformer: Transformer = async (source, options) => {
if (from.endsWith('.sass')) {
const result = await render({
// Use indented syntax.
// ref: https://sass-lang.com/documentation/syntax#the-indented-syntax
indentedSyntax: true,
data: source,
file: options.from,
outFile: 'DUMMY',
// Output sourceMap.
sourceMap: true,
// Resolve import specifier using resolver.
importer: (url, prev, done) => {
options
.resolver(url, { request: prev })
.then((resolved) => done({ file: resolved }))
.catch((e) => done(e));
},
});
return { css: result.css, map: result.sourceMap!, dependencies: result.loadedUrls };
}
// Fallback to default transformer.
return await defaultTransformer(source, from);
};
run({ ...parseArgv(process.argv), transformer }).catch((e) => {
console.error(e);
process.exit(1);
});
Example: Custom resolver
With the resolver
option, you can customize the resolution algorithm for import specifier (such as @import "specifier"
).
#!/usr/bin/env ts-node
import { run, parseArgv, createDefaultResolver, type Resolver } from 'happy-css-modules';
import { exists } from 'fs/promises';
import { resolve, join } from 'path';
const cwd = process.cwd();
const runnerOptions = parseArgv(process.argv);
const { sassLoadPaths, lessIncludePaths, webpackResolveAlias } = runnerOptions;
// Some runner options must be passed to the default resolver.
const defaultResolver = createDefaultResolver({ cwd, sassLoadPaths, lessIncludePaths, webpackResolveAlias });
const stylesDir = resolve(__dirname, 'src/styles');
const resolver: Resolver = async (specifier, options) => {
// If the default resolver cannot resolve, fallback to a customized resolve algorithm.
const resolvedByDefaultResolver = await defaultResolver(specifier, options);
if (resolvedByDefaultResolver === false) {
// Search for files in `src/styles` directory.
const path = join(stylesDir, specifier);
if (await exists(path)) return path;
}
// Returns `false` if specifier cannot be resolved.
return false;
};
run({ ...runnerOptions, resolver, cwd }).catch((e) => {
console.error(e);
process.exit(1);
});
Example: Get locations for selectors exported by CSS Modules
Locator
can be used to get location for selectors exported by CSS Modules.
import { Locator } from 'happy-css-modules';
import { resolve } from 'path';
import assert from 'assert';
const locator = new Locator({
// You can customize the transformer and resolver used by the locator.
// transformer: createDefaultTransformer(),
// resolver: createDefaultResolver(),
});
// Process https://github.com/mizdra/happy-css-modules/blob/main/packages/example/02-import/2.css
const filePath = resolve('example/02-import/2.css'); // Convert to absolute path
const result = await locator.load(filePath);
assert.deepEqual(result, {
dependencies: ['/Users/mizdra/src/github.com/mizdra/packages/example/02-import/3.css'],
tokens: [
{
name: 'b',
originalLocations: [
{
filePath: '/Users/mizdra/src/github.com/mizdra/packages/example/02-import/3.css',
start: { line: 1, column: 1 },
end: { line: 1, column: 2 },
},
],
},
{
name: 'a',
originalLocations: [
{
filePath: '/Users/mizdra/src/github.com/mizdra/packages/example/02-import/2.css',
start: { line: 3, column: 1 },
end: { line: 3, column: 2 },
},
],
},
],
});
About the origins of this project
This project was born as a PoC for Quramy/typed-css-modules#177. That is why this project forks Quramy/typed-css-modules
. Due to refactoring, only a small amount of code now comes from Quramy/typed-css-modules
, but its contributions can still be found in the credits of the license.
Thank you @Quramy!
Prior art
There is a lot of excellent prior art.
- โ Supported
- ๐ถ Partially supported
- ๐ Not supported
- โ Unknown
Repository | Strict type checking | Definition jumps | Sass | Less | resolve.alias |
How implemented |
---|---|---|---|---|---|---|
Quramy/typed-css-modules | โ | ๐ | ๐ | ๐ | ๐ | CLI Tool |
skovy/typed-scss-modules | โ | ๐ | โ | ๐ | ๐ | CLI Tool |
qiniu/typed-less-modules | โ | ๐ | ๐ | โ | ๐ | CLI Tool |
mrmckeb/typescript-plugin-css-modules | ๐ถ*1 | ๐ถ*2 | โ | โ | ๐ | TypeScript Language Service*3 |
clinyong/vscode-css-modules | ๐ | โ | โ | โ | ๐ | VSCode Extension |
Viijay-Kr/react-ts-css | ๐ถ*1 | โ | โ | โ | โ | VSCode Extension |
mizdra/happy-css-modules | โ | โ | โ | โ | โ | CLI Tool + Declaration Map |
- *1: Warnings are displayed in the editor, but not at compile time.
- *2: Not supported for
.less
definition jumps. - *3: The TypeScript language service can display warnings in the editor, but not at compile time. It is also complicated to set up.
Another known tool for generating .css.d.ts
is wix/stylable , which does not use CSS Modules.