• Stars
    star
    141
  • Rank 259,971 (Top 6 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 2 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Typed, definition jumpable CSS Modules. Moreover, easy!

Cover image

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
  • ๐Ÿ” 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).
  • ๐Ÿค 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.

Illustration of how definition jump works

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.

Illustration of a case with multiple definitions

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.

Example: Custom hcm commands

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.

More Repositories

1

eslint-interactive

The CLI tool to fix huge number of ESLint errors
TypeScript
269
star
2

wasm-dev-book

Rust ใ‚’็”จใ„ใŸ WebAssembly ใฎ้–‹็™บ็’ฐๅขƒใ‚’ๆง‹็ฏ‰ใ™ใ‚‹ๆ‰‹ๆณ•ใ‚’็ดนไป‹ใ™ใ‚‹ๆœฌ.
JavaScript
40
star
3

typescript-plugin-asset

TypeScript language service plugin supporting for importing assets.
TypeScript
32
star
4

eslint-plugin-layout-shift

ESLint plugin to force responsive media elements to set the width/height attributes
JavaScript
21
star
5

graphql-codegen-typescript-fabbrica

GraphQL Code Generator Plugin to define mock data factory.
TypeScript
12
star
6

dotfiles

Dotfiles for @mizdra
Shell
10
star
7

scrapbox-userscript-icon-suggestion

Scrapbox's UserScript to suggest and insert icons
TypeScript
9
star
8

now-playing-for-google-play-music

#NowPlaying for YouTube Music
TypeScript
9
star
9

npm-package-template

TypeScript
8
star
10

volar-demo

TypeScript
8
star
11

inline-fixture-files

A utility to write filesystem fixtures inline.
TypeScript
6
star
12

img-layout-shift-detector

The browser extension that detects `<img>` tags that cause Layout Shift
TypeScript
5
star
13

eslint-config-mizdra

ESLint config for @mizdra
JavaScript
3
star
14

strictly-typed-event-target

This is a strictly-typed version of `EventTarget`
TypeScript
3
star
15

rocketimer

[experimental] ๐Ÿš€Blazing fast cascade timer
TypeScript
3
star
16

shiki

Shiki is a rust-like programming language.
Rust
2
star
17

rust-tinymt

The Rust implements of TinyMT for Pokรฉmon RNG.
Rust
2
star
18

webpack-ts-skeleton

TypeScriptใฎ็ด ๆŒฏใ‚Š็’ฐๅขƒ
JavaScript
2
star
19

react-component-definition-jump-test

Created with CodeSandbox
TypeScript
2
star
20

webpack-wasm-skeleton

WebAssemblyใฎ็ด ๆŒฏใ‚Š็’ฐๅขƒ
JavaScript
1
star
21

happy-birthday

Happy Birthday, mizdra ๐ŸŽ‰
JavaScript
1
star
22

pokemon-swsh-scripts

Python
1
star
23

yukari-slot

ใ€Žใ‚†ใ‚†ๅผใ€ใฎๆ—ฅๅ‘็ธใ•ใ‚“ใฎ่ช•็”Ÿๆ—ฅใ‚’ใŠ็ฅใ„ใ—ใฆไฝœๆˆใ—ใŸใ‚นใƒญใƒƒใƒˆใ‚ฒใƒผใƒ 
HTML
1
star
24

parcel-frontend-skeleton

Boilerplate for Web frontend.
JavaScript
1
star
25

stylelint-happy-css-modules

TypeScript
1
star
26

prettier-config-mizdra

Shareable Prettier Config for @mizdra
JavaScript
1
star
27

Perl-Tidy-LanguageServer

Language Server for Perltidy.
Perl
1
star
28

renovate-config-mizdra

Shareable Renovate Config for @mizdra
1
star
29

nextjs-get-server-side-props-util-example

CSS
1
star
30

wasm-dev-book-webpack

Webpackใ‚’ไฝฟใฃใŸWebAssemblyใฎ้–‹็™บ็’ฐๅขƒ.
Rust
1
star
31

wasm-dev-book-hello-wasm

WebAssemblyใฎๅ…ฅ้–€.
HTML
1
star