Fellow fork: css-modules webpack-parity hashing + GH Packages publishing#2
Merged
Fellow fork: css-modules webpack-parity hashing + GH Packages publishing#2
Conversation
These will back new HashAlgorithm options in the next commit, enabling webpack/css-loader-compatible scoped-name hashes.
…tions helper Introduces a parameterized hash-and-encode helper for CSS module names, with md4 and xxhash64 algorithms and standard-base64 / hex digests. The default [hash] / [content-hash] code path is unchanged; this commit only adds the new types and helper. Pattern parsing and Segment wiring follow in subsequent commits. Tested against ground-truth output captured from a Vite/postcss-modules build to confirm md4+base64+truncate behaves identically to webpack's loader-utils.getHashDigest.
…nd length Segment::Hash becomes a struct variant carrying optional HashAlgorithm, DigestType, and length fields. When all are None the legacy siphash + custom-base64 path is preserved (existing test snapshots unchanged); when any is Some, hashing dispatches through hash_with_options. Hash computation moves from CssModule::new to Pattern::write so each segment can apply its own options. CssModule::hashes is renamed to hash_inputs and now stores the raw source-relative path string instead of a precomputed hash. Pattern::write computes the hash per segment from this raw input. Segment::ContentHash remains a unit variant in this commit; per-segment options on [content-hash] would require additional plumbing through the bundler's content-hash precomputation and is left for a follow-up.
…] syntax The parser now recognizes webpack-compatible scoped-name hash placeholders. Each of algo, digest, and length is optional; the position of the literal \`hash\` keyword distinguishes algo (before) from digest+length (after). Examples: [hash] -> legacy lightningcss hash, unchanged [hash:base64] -> default algo, base64 digest, full length [hash:5] -> default algo, hex digest, 5 chars [md4:hash:base64:5] -> md4 + base64 + 5 chars (matches webpack) [xxhash64:hash:hex:12] -> xxhash64 + hex + 12 chars Bare \`[hash]\` keeps the legacy code path so existing snapshots remain byte- identical. Recognized algorithms: md4, xxhash64. Recognized digests: hex, base64. Parsing is case-insensitive. Unknown algos/digests, missing \`hash\` keyword, and extra parts before \`hash\` all return UnknownPlaceholder. Tests cover each form, case-insensitivity, rejection of bad input, and a round-trip integration test against ground-truth output captured from a Vite/postcss-modules build (digest only; the css-loader content composition and post-processing pipeline live downstream of this commit).
Adds a hash_prefix field to css_modules::Config that prepends a string to every hash input. This matches Vite/postcss-modules' hashPrefix option and, when set to '\x00\x00\x00\x00', reproduces css-loader's tier-0 salt input byte-for-byte — a necessary piece for Vite-to-webpack scoped-name parity. Default is None, leaving lightningcss output unchanged for existing users. Per-local hash composition and post-processing (also required for full Vite parity) follow in subsequent commits.
Adds a hash_local_name flag to css_modules::Config. When true, the local class/ident name is appended to the hash input separated by a NUL byte: <prefix><relative-path>\0<local>. This matches the per-local hashing done by css-loader and postcss-modules — without it every export from a given file shares one hash. Composition happens via a new CssModule::hash_input_for helper, called from each Pattern::write call site (add_local, add_dashed, reference, reference_dashed, handle_composes, plus printer's write_ident and write_dashed_ident). Each call allocates a small string when hash_local_name is enabled; otherwise it returns Cow::Borrowed against the existing buffer. Default is false, preserving lightningcss's per-file hashing for users who don't opt in. Combined with hash_prefix from the previous commit, the bytes fed to the hash now match css-loader's input bytes exactly. A test asserts that Md4+Base64+5 over the composed input reproduces the captured Vite digest 'YTbdH' for src/styles/Alpha.module.css#foo.
Adds an escape_scoped_names config flag and an escape_scoped_name helper that mirrors css-loader/postcss-modules' genericNames post-processing: s.replace(/[^a-zA-Z0-9\\-_\\u00A0-\\uFFFF]/g, '-') .replace(/^((-?[0-9])|--)/, '_\$1') The pipeline runs after Pattern::write_to_string at every scoped-name call site (CssModule::add_local/add_dashed/reference/reference_dashed/handle_composes plus Printer::write_ident/write_dashed_ident). For dashed (custom property) idents the leading '--' is excluded — the rendered pattern is escaped without the prefix and '--' is prepended afterwards, so '--' on a custom property never triggers the leading-double-dash rule. write_to_string is also promoted from private to pub(crate) so the printer can call it directly (replacing the previous streaming Pattern::write + serialize_identifier/serialize_name closure pattern). Output is unchanged for the legacy (no-escape) path: serialize_identifier on a full string produces the same bytes as serialize_identifier on the head + serialize_name on the tail. Eight unit tests cover the four post-process branches captured from the real Vite/postcss-modules build (digit, +, /, leading -digit, leading --, leading -letter unchanged, clean input, unicode above U+00A0).
Integrates the four hashing features (md4/xxhash64 algos, hash_prefix, hash_local_name, escape_scoped_names) and asserts the rendered scoped names match a Vite/postcss-modules build's output byte-for-byte, using the generateScopedName='[name]__[local]__[md4:hash:base64:5]' and hashPrefix='\\0\\0\\0\\0' configuration captured from a real project. Twelve cases across two source files cover every post-processing branch: clean digest, +/-/-cleanup-mid-name, leading-digit prefix on rendered output, leading -digit prefix, leading -- prefix, and leading -letter (no prefix). If this test breaks, lightningcss has diverged from webpack/css-loader hashing and Vite migrations relying on hash stability will produce different class names than they did pre-migration.
GitHub Packages requires scoped names matching the publishing org. Rename the top-level package and the eleven platform-specific binary packages from 'lightningcss[-platform]' to '@fellowapp/lightningcss[-platform]', update node/index.js to require the scoped platform package at runtime, and point publishConfig.registry at https://npm.pkg.github.com. Drop CLI packaging from build-npm.js — the fork is consumed as a library (via Vite/lightningcss-napi), not as a CLI, and the per-platform CLI bundles are dead weight. Version bumped to 1.32.0-fellow.0 so the fork doesn't shadow upstream.
Adapted from .github/workflows/release.yml. Builds the same matrix of
platform-specific .node binaries (10 targets: macOS x64/arm64, Windows
x64/arm64, Linux x64/arm64 gnu+musl, Linux armv7, Android arm64) and
publishes them to GitHub Packages as @fellowapp/lightningcss-<platform>,
plus the main @fellowapp/lightningcss package.
Differences from the upstream workflow:
- Triggered on workflow_dispatch only (no automated tag-based release).
- Drops FreeBSD and wasm targets (not needed for the Vite-via-napi use case).
- Drops CLI build/publish (the fork is consumed as a library).
- Authenticates with GITHUB_TOKEN against npm.pkg.github.com via
setup-node's registry-url + scope inputs.
- Requires permissions: { packages: write, contents: read } on the
release job.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Forks
lightningcssto add four CSS-modules scoped-name hashing features needed for byte-parity with webpack/css-loader/postcss-modules, and packages the result for distribution via GitHub Packages as@fellowapp/lightningcss.The first 4 commits are upstreamable as-is and will be sent to
parcel-bundler/lightningcssseparately (see branchfeat/css-modules-hash-algorithms). The remaining commits are Fellow-specific.CSS modules changes (commits 1-8)
feat(css-modules): add HashAlgorithm and DigestType+hash_with_optionshelper (md4, xxhash64; hex, standard base64)feat(css-modules): extend Segment::Hash with optional algo, digest, length(struct variant)feat(css-modules): parse webpack-style [<algo>:hash:<digest>:<length>] syntaxfeat(css-modules): add Config::hash_prefix(Vite/postcss-modules'hashPrefix; matches css-loader tier-0 salt)feat(css-modules): add Config::hash_local_name(per-(file, local) hashing)feat(css-modules): add Config::escape_scoped_names(post-process pipeline making +/ safe and prefixing leading-digit/-- with _)test(css-modules): add Vite scoped-name byte-parity end-to-end test(12 captured cases reproduce byte-for-byte)All defaults preserve existing lightningcss output: each Fellow-specific feature is an opt-in flag.
Packaging changes (commits 9-10)
chore(npm): rename package to @fellowapp/lightningcss for GH Packages— scoped names + GH Packages registry + dropped CLI publishingci: add release-fellowapp workflow to publish to GitHub Packages—workflow_dispatch-triggered build matrix (10 platform targets) authenticating withGITHUB_TOKENTest plan
cargo test --no-default-features— 135 pass (only pre-existing platform-dependenttest_dependenciesfailure remains, unrelated to this work)@fellowapp/lightningcss@1.32.0-fellow.0resolves from GH Packagespnpm.overrides