Skip to content

Add Node ESM loader hook for import styles from './x.css'#169

Merged
madyankin merged 2 commits into
masterfrom
postcss-modules-js-import
Jun 30, 2026
Merged

Add Node ESM loader hook for import styles from './x.css'#169
madyankin merged 2 commits into
masterfrom
postcss-modules-js-import

Conversation

@madyankin

@madyankin madyankin commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds an opt-in postcss-modules/loader entrypoint — a Node module-customization hook that lets JavaScript files do import styles from './foo.css' and receive the token map directly. Mirrors the workflow css-loader and Vite's built-in CSS-Modules handling already provide.
  • Aimed at non-bundler use cases (SSR, Node scripts, Jest/Vitest in node mode). Bundler users keep using their existing integrations; nothing about the existing plugin path changes.
  • Options resolved from postcss-modules.config.{js,cjs,mjs} in cwd, POSTCSS_MODULES_CONFIG env var, or a "postcss-modules" key in package.json. Same shape as the PostCSS plugin options.
  • Closes Deeper integration with postcss-loader #80.

Usage

node --import postcss-modules/loader app.mjs
// app.mjs
import styles from "./button.css";
console.log(styles.primary); // "_button__primary_xkpkl_5"

Implementation notes

  • src/loader.mjs — entry; calls module.register on the hooks module.
  • src/loaderHooks.mjsload hook intercepts file://*.css URLs, runs them through the existing plugin with a getJSON callback that captures tokens, returns a synthetic ESM source.
  • src/processCSS.js — small helper that sets up the fs adapter and invokes the existing makePlugin pipeline (no new code path through the transformer).
  • src/loadConfig.mjs — config resolver. Kept as .mjs so swc doesn't rewrite its dynamic import() to require().
  • Makefile gets a compile target; swc ignores *.mjs and we cp those verbatim into build/.
  • package.json exports map adds "./loader". engines.node bumped to >=20.6 (where module.register stabilized).

Limitations (called out in the README)

  • Returns the token map only; the transformed CSS is not emitted. SSR users who also need the CSS should use a bundler.
  • No HMR / watch mode — by design.

Test plan

  • make test — 42 passing (5 new loader cases + 37 existing), lint clean.
  • New cases cover: simple class set, composes-across-files, postcss-modules.config.cjs discovery from cwd, POSTCSS_MODULES_CONFIG env var override, missing-file error surfaces the source path.
  • Smoke test against a real consumer:
    mkdir /tmp/pcm && cd /tmp/pcm && npm init -y && npm link postcss-modules
    echo '.primary { color: red; }' > button.css
    echo 'import s from "./button.css"; console.log(s);' > app.mjs
    node --import postcss-modules/loader app.mjs

@madyankin
madyankin force-pushed the postcss-modules-js-import branch from 8b3806f to af0a3c5 Compare June 30, 2026 19:30
Ships an opt-in `postcss-modules/loader` entrypoint that registers a
Node module-customization hook. With `node --import postcss-modules/loader`,
JavaScript files can `import styles from './foo.css'` and receive the
class-name token map directly, mirroring the workflow `css-loader` and
similar bundler integrations provide.

The hook reads the CSS file, runs it through the existing plugin with a
synthetic `getJSON` callback that captures tokens, and returns a synthetic
ESM module whose default export is the token map. Options are resolved
from `postcss-modules.config.{js,cjs,mjs}` in cwd, the `POSTCSS_MODULES_CONFIG`
env var, or a `"postcss-modules"` key in `package.json`, and accept the
same shape as the PostCSS plugin.

Targeted at non-bundler use cases (SSR, Node scripts, Jest/Vitest in
node mode); bundler users keep using `css-loader`, Vite's built-in
support, etc. Requires Node >= 20.6 where `module.register` stabilized.

Closes #80.
@madyankin
madyankin force-pushed the postcss-modules-js-import branch from d929ed0 to 2de0d0c Compare June 30, 2026 19:38
Add a configuration sub-section with concrete examples (config file,
package.json key, env var), Jest/Vitest setup snippet, an ambient
TypeScript declaration, and an explicit limitations list (ESM-only,
tokens-only output, no HMR, Node version).
@madyankin
madyankin force-pushed the postcss-modules-js-import branch from 2de0d0c to 82da0e6 Compare June 30, 2026 19:43
@madyankin
madyankin merged commit 5745f42 into master Jun 30, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deeper integration with postcss-loader

1 participant