Skip to content

Move CSS linting from server to client-side - #367

Open
bartveneman wants to merge 2 commits into
mainfrom
claude/browser-stylelint-lint-css-i1zhwh
Open

Move CSS linting from server to client-side#367
bartveneman wants to merge 2 commits into
mainfrom
claude/browser-stylelint-lint-css-i1zhwh

Conversation

@bartveneman

Copy link
Copy Markdown
Member

Summary

This PR moves stylelint-based CSS linting from a server-side API endpoint to client-side execution in the browser, eliminating the need for a backend linting service while reducing bundle dependencies.

Key Changes

  • New browser-based linting module (src/lib/stylelint-browser/lint.ts): Implements a direct stylelint core runner that bypasses the standalone() entry point and its file-resolution machinery (globby, cosmiconfig, write-file-atomic). This deep-imports stylelint internals (lintPostcssResult, normalizeAllRuleSettings, createPartialStylelintResult, prepareReturnValue) and pins their shapes via TypeScript declarations.

  • Removed server API endpoint (src/routes/api/lint-css/+server.ts): The POST endpoint that previously handled linting requests is no longer needed.

  • Updated Linter component (src/lib/components/Linter.svelte): Refactored to call the new client-side lint() function directly instead of making HTTP requests. CSS fetching (via get_css) remains client-side, and formatting is applied before linting.

  • Vite configuration updates (vite.config.js): Added resolve aliases to:

    • Point deep imports of unpublished stylelint internals to their real files on disk
    • Provide browser-compatible shims for Node modules (node:os, node:process) and stylelint's timing module
    • Alias the bare stylelint specifier to a shim that exposes only the public plugin API
  • Browser shims (new files in src/lib/stylelint-browser/shims/):

    • node-os.ts: Minimal EOL export
    • node-process.ts: Stubs for env, noDeprecation, emitWarning, on, hrtime
    • stylelint-timing.ts: No-op timing module to avoid pulling in the table package
    • stylelint-shim.ts: Exposes createPlugin and public utils without loading Node-only dependencies
  • Preset configuration (src/lib/stylelint-browser/presets.ts): Extracted preset-to-rules mapping into a shared module used by both the component and tests.

  • Test updates (src/routes/(public)/lint-css/spec.ts): Updated to mock the CSS-fetching API instead of the linting API, and adjusted assertions to match real stylelint output.

  • Unit tests (src/lib/stylelint-browser/lint.test.ts): Added smoke tests verifying the deep-imported stylelint internals work correctly with the plugin.

Notable Implementation Details

  • TypeScript declarations (stylelint-internals.d.ts) manually pin the shapes of unpublished stylelint internals to stylelint ~17.13.0, with instructions to re-verify on version bumps.
  • Vite's resolve.alias is used (not a custom plugin resolver) because the dependency optimizer pre-bundles packages before the plugin pipeline runs.
  • The aliases apply globally rather than being scoped to the client build, which is safe since the code only executes in the browser and just needs to resolve for SSR.
  • No extends resolution, custom syntaxes, caching, ignore files, or autofix support—the implementation focuses on the core linting use case.

https://claude.ai/code/session_015YSKEQXzpmfmag1HcDNUaQ

Drives stylelint's linting core directly (lintPostcssResult,
normalizeAllRuleSettings, createPartialStylelintResult, prepareReturnValue)
instead of routing through standalone()/lint(), which statically pulls in
globby, cosmiconfig and other Node-only deps. The four internals aren't part
of stylelint's public exports map, so they're aliased to the real files on
disk in vite.config.js and pinned to stylelint ~17.13.0 via
stylelint-internals.d.ts.

Replaces the /api/lint-css POST round-trip: the Linter component now fetches
CSS (for URL input) via the existing client-side get_css() helper and lints
it in-browser with src/lib/stylelint-browser/lint.ts, cutting out a full
server round-trip per lint. CSS-scraping stays server-side in /api/get-css
(Node-only deps, CORS).

Includes browser-safe shims for node:os, node:process and stylelint's own
bare-specifier entry point (needed because @projectwallace/stylelint-plugin's
rule files import `stylelint` just to reach createPlugin/utils), plus a stub
for stylelint's CLI timing/table output which isn't reachable from the
browser and doesn't bundle cleanly.
@netlify

netlify Bot commented Aug 9, 2026

Copy link
Copy Markdown

Deploy Preview for projectwallace ready!

Name Link
🔨 Latest commit da3d8e6
🔍 Latest deploy log https://app.netlify.com/projects/projectwallace/deploys/6a78df911162140008f94d1d
😎 Deploy Preview https://deploy-preview-367--projectwallace.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Member Author

The Audit check is failing on the pre-existing high-severity nanoid advisory (GHSA-2v37-7h3g-55p8, nanoid < 3.3.17), reached transitively through postcss (via vite, postcss-html, and now also stylelint). I checked out main's package.json/pnpm-lock.yaml and ran pnpm audit --audit-level=high against it directly — same failure, same advisory, same root cause. Not introduced by this PR (promoting stylelint/postcss to direct dependencies adds one more graph edge to the same already-vulnerable nanoid, not a new vulnerability). No action taken here; will keep watching in case the base branch resolves it.


Generated by Claude Code

…able

Added a dedicated pinned-transitive-deps.ts, imported for its side effect
from lint.ts, that imports css-tree, @csstools/css-syntax-patches-for-csstree,
@csstools/css-tokenizer and fastest-levenshtein - packages stylelint's own
internals (getLexer.mjs, assignDisabledRanges.mjs, reportUnknownRuleNames.mjs)
import by name but our own code never did directly. Without a real usage
site, dependency-usage tooling has no way to tell they're needed rather than
stray entries, and pnpm's strict node_modules requires them declared in our
own package.json regardless (they're normally hoisted transitively, but only
for packages we actually import from).

Also drops `table` from dependencies: it was added while investigating why
stylelint/lib/timing.mjs doesn't bundle for the browser, but the fix ended up
aliasing that whole relative import away (see vite.config.js), so the real
`table` package was never reachable to begin with.
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.

2 participants