Skip to content

fix: stylelint config for nested installs and Vue style blocks - #100

Merged
yokuze merged 6 commits into
silvermine:masterfrom
kmuncie:kmuncie/stylelint-vue-and-nested-installs
Oct 16, 2026
Merged

yokuze merged 6 commits into
silvermine:masterfrom
kmuncie:kmuncie/stylelint-vue-and-nested-installs

Conversation

@kmuncie

@kmuncie kmuncie commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Why

Projects that upgrade from v2 of this package to v3.0.0 with an existing package-lock.json end up with postcss, postcss-html and postcss-scss nested under @silvermine/standardization or stylelint and no copy at the top level of node_modules (npm keeps the old v2 packages in the top-level slots while it builds the new tree, then prunes them). In that layout npm run stylelint fails before linting a single file:

  • @stylistic/stylelint-plugin@3.1.2 imports postcss without declaring it, so it fails with Cannot find package 'postcss'.
  • Stylelint imports a customSyntax given as a bare package name from its own location, so postcss-scss and postcss-html fail with Cannot resolve custom syntax module.

I hit both while upgrading a consumer project from 2.2.3 to 3.0.0. Projects already on 3.0.0 work only when their lockfiles happen to hoist those packages. On top of that, the **/*.vue override did not turn off the CSS-only rules that the **/*.scss override turns off (#97), and v-bind() values tripped value-keyword-case (#94).

What

One commit per change, each passing npm test and npm run standards on its own:

  1. build: upgrade @stylistic/stylelint-plugin to 3.1.3 – 3.1.3 declares postcss as a dependency and moves stylelint to a peer dependency (bugfix-only release). The lockfile diff is kept to the plugin's own entry on purpose, see "Lockfile note" below.
  2. fix: resolve stylelint custom syntaxes by absolute path – the configuration moves into stylelint-config.cjs, where require.resolve() turns both parsers into absolute paths. .stylelintrc.yml now only extends that file, so consumers keep extending the same entry point. README tells consumers not to set customSyntax themselves.
  3. fix: apply the SCSS rule overrides to Vue style blocks (#97) – both overrides share one SCSS rule set. The test runner now also runs .vue fixtures; the new pass fixture fails against the previous config.
  4. fix: allow any case inside v-bind() in Vue style values (#94) – value-keyword-case ignores v-bind(), with a fixture.
  5. fix: disable markdownlint descriptive-link-text rule – ported from Update markdownlint and stylelint to the latest #96. markdownlint 0.38 (what our pinned cli2 ships) enables MD059, and our repos' docs use link text such as "here" in several places.
  6. style: indent package.json and its lockfile with 3 spaces – ported from Update markdownlint and stylelint to the latest #96, whitespace only. Matches .editorconfig; npm keeps the indentation it finds and formats the lockfile to match.

Fixes #97
Fixes #94

Verification

  • npm test and npm run standards pass locally and in CI.
  • Packed this branch, installed it in a scratch project, and moved the parsers into the nested layout that a consumer's lockfile produces. With that install, stylelint lints all 219 style files of a consumer project (31 SCSS, 188 Vue) with no resolution errors and no false positives. The remaining 6 reports are real violations in that project. The same chain also works through the one-line .stylelintrc.yml that most of our repos use, with --config-basedir, and through both the CommonJS and ESM Node APIs (what webpack and Grunt plugins call).
  • The same nested layout with the published 3.0.0 fails with the two errors above.

Why .cjs and not .mjs

The format of the shared config is invisible to consumers: they extend the yml and stylelint loads whatever it points at. An .mjs version of the same file (using createRequire(import.meta.url)) behaves identically in the same tests, and the .cjs extension is explicitly CommonJS, so ESM consumers ("type": "module") are unaffected either way.

I chose .cjs for consistency with this repo today: the release scripts, bin, commitlint config and markdownlint config are all CommonJS, and ESLint 6 here lints CommonJS. Our newest repos are ESM and stylelint recommends ESM configs, so the natural follow-up is a repo-wide move to ESM, at which point this file becomes stylelint-config.mjs with a three-line change and consumers notice nothing.

Relation to open PR #96

#96 moves to stylelint 17 and conflicts with master. This PR stays on stylelint 16.20.0, so it can land first. Two of its smaller changes are ported here (commits 5 and 6). Not ported, on purpose:

  • The stylelint 17 / config-standard 40 bump. That is a separate decision with its own consumer impact (nesting and specificity rule changes, Node ≥ 20.19, @stylistic 5.x required).
  • nesting-selector-no-missing-scoping-root with ignoreAtRules: [mixin]. The rule only exists from stylelint 16.23 and config-recommended enables it from 17.0.0, so it belongs with that bump. Our SCSS uses & inside @mixin blocks a lot, so the ignoreAtRules part will be needed then.
  • The markdownlint-cli2 0.21 bump. Its stated reason, the gitignore option, is already in use since 3.0.0 with cli2 0.18.1.

Future work

Lockfile note

npm install in this repo (with the pinned npm 11.6.1 as well as newer npm) drops the lock entry node_modules/stylelint/node_modules/typescript, an optional peer of cosmiconfig. CI's npm ci then rejects the lock as out of sync and lists missing @typescript/typescript-* packages. The lock here keeps that entry and changes only what the plugin bump needs; npx npm@11.6.1 ci passes on it.

Notes for consumers

  • A project config that sets customSyntax: postcss-html itself (some do, from the v2 days when the shared config had no Vue override) still fails in the nested layout. Drop that line; the shared config covers .vue files.
  • Run npm ci after bumping. A stale node_modules that still holds v2's postcss-scss makes stylelint 16 crash with Cannot read properties of undefined (reading 'nodes'), because postcss-html looks for language parsers in the working directory first.

@kmuncie

kmuncie commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

I would recommend merging #99 before this so the package upgrades can be done on the correct Node version

@kmuncie
kmuncie force-pushed the kmuncie/stylelint-vue-and-nested-installs branch from 7db5866 to 9b43d13 Compare October 14, 2026 17:16

@yokuze yokuze left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also needs rebase to fix merge conflicts.

Comment thread README.md Outdated
```

The `<style>` blocks of `.vue` single-file components are linted with the same rules as
`.scss` files. A `.vue` file without a `<style>` block is empty input to stylelint, which

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A style-less .vue file is not empty input, so this sentence gives the wrong reason for --allow-empty-input. Could it say the flag keeps stylelint from failing when the glob matches no files?

Details

With stylelint 16.20.0 and this branch installed in a scratch project:

  • stylelint './src/**/*.{scss,vue}' exits 0 when src/ contains only a .vue file with no <style> block.
  • The same command exits with NoFilesFoundError: No files matching the pattern "./src/**/*.{scss,vue}" were found. when src/ has no .scss or .vue files.

Suggested wording:

The `<style>` blocks of `.vue` single-file components are linted with the same rules as
`.scss` files. The example passes `--allow-empty-input` so that the command does not fail
in a project that has no `.scss` or `.vue` files.

Comment added by AI model Claude

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and confirmed: a .vue file with no <style> block exits 0 without the flag, and only a glob with no matches fails. I used your suggested wording in 3222428.

Comment added by AI model Claude

Comment thread stylelint-config.cjs
},
{
files: [ '**/*.vue' ],
customSyntax: require.resolve('postcss-html'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This override also applies the SCSS rule set to plain <style> blocks with no lang, so CSS-only checks no longer run there. Is that intended?

Details

declaration-property-value-no-unknown, function-no-unknown, and media-query-no-invalid are off for every <style> block in a .vue file. With this branch, color: rad; inside <style> (no lang) passes, while the same line in a .css file reports Unexpected unknown value "rad" for property "color". On master, the .vue block reported it too.

If all Vue style blocks are expected to use lang="scss", this is fine, and a sentence in the README would make that explicit. Otherwise, postcss-html cannot pick rules per lang, so the trade-off may only need a note.

Comment added by AI model Claude

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is intended. Our Vue components use <style lang="scss">, and overrides select rules per file, not per lang, so there is no way to keep the CSS-only checks for a plain block in the same file. In 3222428 the README now says that Vue style blocks must use lang="scss" and names the checks that do not run on a plain <style> block. The comment above SCSS_RULES in stylelint-config.cjs says the same.

Comment added by AI model Claude

Version 3.1.2 imported postcss without declaring it as a dependency, so it
relied on npm hoisting stylelint's copy of postcss next to it. When a project
upgrades from v2 with an existing package-lock.json, npm nests postcss under
stylelint instead, and the plugin fails with "Cannot find package 'postcss'".
Version 3.1.3 declares postcss as a dependency and moves stylelint to a peer
dependency.
Stylelint imports a `customSyntax` given as a bare package name from its own
location in node_modules, not from the config file that names it. When npm
nests postcss-scss and postcss-html under @silvermine/standardization instead
of hoisting them next to stylelint, linting fails with "Cannot resolve custom
syntax module". That is the layout npm produces when a project upgrades from
v2 of this package with an existing package-lock.json.

Move the configuration into `stylelint-config.cjs`, where `require.resolve()`
turns both parsers into absolute paths. `.stylelintrc.yml` now only extends
that file, so projects keep extending the same entry point as before.
@kmuncie
kmuncie force-pushed the kmuncie/stylelint-vue-and-nested-installs branch from 14b7d95 to 6e293f9 Compare October 15, 2026 19:17
The `**/*.vue` override only turned off `at-rule-no-unknown`, so the CSS-only
rules that the `**/*.scss` override disables (for example
`declaration-property-value-no-unknown` and `function-no-unknown`) still ran
against the `<style lang="scss">` blocks of Vue components. A SCSS variable in
a `transition` value was reported as an unknown value. Both overrides now share
the same SCSS rule set, so a Vue style block is linted exactly like a `.scss`
file.

The stylelint test runner now also picks up `.vue` fixtures. The new pass
fixture fails against the previous config, and the fail fixture proves that
Vue style blocks are linted at all.

Fixes silvermine#97
Vue's `v-bind()` in CSS takes the name of a reactive binding from the
component's script, which follows our camelCase naming for TypeScript
constants. `value-keyword-case` reported those names as uppercase keywords.
Ignore the rule inside `v-bind()` only, as proposed in the issue.

Fixes silvermine#94
markdownlint 0.38 added MD059, which rejects link text such as "here" or
"this page". Our documentation uses that style in several repositories, so
projects would start failing `npm run markdownlint` when they pick up this
package. Turn the rule off in the shared config.

Proposed in silvermine#96.
The repo's .editorconfig asks for 3-space indentation in every file, but
package.json still used the 2 spaces that `npm init` wrote years ago. npm
keeps whatever indentation it finds in package.json, and it formats
package-lock.json to match, so re-indent both now. Whitespace-only change.

Proposed in silvermine#96.
@kmuncie
kmuncie force-pushed the kmuncie/stylelint-vue-and-nested-installs branch from 6e293f9 to 3af3611 Compare October 15, 2026 20:18
@kmuncie
kmuncie requested a review from yokuze October 15, 2026 20:20
@yokuze
yokuze merged commit da29495 into silvermine:master Oct 16, 2026
5 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.

Ensure all SCSS stylelint rules apply to *.vue files too Allow alternate cases in Vue's v-bind statements for CSS/SCSS in .vue files

2 participants