Skip to content

[heft-sass-plugin] Add opt-in bare specifier resolution options - #6094

Merged
Ian Clanton-Thuon (iclanton) merged 2 commits into
microsoft:mainfrom
iclanton:fix-sass-bare-specifier-resolution
Sep 28, 2026
Merged

Ian Clanton-Thuon (iclanton) merged 2 commits into
microsoft:mainfrom
iclanton:fix-sass-bare-specifier-resolution

Conversation

@iclanton

@iclanton Ian Clanton-Thuon (iclanton) commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds two opt-in options for resolving bare specifiers in Sass stylesheets, e.g. @use '@scope/pkg/theme', and fixes the legacy ~ rewrite in constructs other than @use/@import/@forward.

Revised after review. This PR originally made node_modules resolution of bare specifiers automatic and described it as fixing a regression. As David Michon (@dmichon-msft) pointed out, that framing was wrong: per the Sass specification the target of @use, @import and @forward is a URL, so @scope/pkg/theme is a relative path, and the existing behavior was by design. Both fallbacks are now off by default and apply only when explicitly configured.

Motivation

Some other Sass toolchains — the Dart Sass CLI's --load-path, sass-loader, Vite, the Angular CLI — resolve bare specifiers from node_modules. Stylesheets authored for those toolchains rely on it, and the import is frequently inside a third-party package, where a consuming project cannot rewrite it. Today there is no way to consume such a package: loadPaths was never exposed, and sass.json is additionalProperties: false, so there is no escape hatch.

These options provide one without changing the default behavior for anyone else. Reported downstream against SPFx 1.23 in microsoft/sp-dev-docs#11030.

Changes

  • loadPaths — folders, resolved against the project folder, searched when a bare specifier does not resolve relative to the importing file. Analogous to the Sass compiler option of the same name.
  • resolveBareSpecifiersAsPackages — when true, a bare specifier that resolves neither relatively nor from loadPaths is additionally resolved as a package via Node module resolution. Defaults to false.
  • Both apply only after relative resolution has failed, so enabling them cannot change the meaning of a specifier that already resolves.
  • A package-resolution failure returns null rather than propagating, so Sass reports its usual Can't find stylesheet to import pointing at the offending line, instead of an internal Cannot find package "...".
  • Legacy ~ handled in the resolver. Previously the ~ → pkg: rewrite was a regex over @import/@use/@forward only, and any surviving tilde threw Unexpected tilde in URL. The rewrite now also happens during canonicalization, so ~ works in @include meta.load-css('~@scope/pkg'). ~ is an explicit package reference, so it is not gated behind resolveBareSpecifiersAsPackages.

Testing

heft test — 65 passing, 8 new.

New coverage: resolution from node_modules when enabled; resolution inside a dependency stylesheet (the reported scenario); ~ inside meta.load-css(); and resolution via loadPaths.

Four are guard tests pinning behavior that must not change:

  • a bare specifier does not resolve as a package by default — this test omits the option entirely, so it asserts the default value rather than an explicit false;
  • a file relative to the importer still wins over a same-named package;
  • a specifier naming no installed package still produces the normal Sass diagnostic;
  • a bare specifier does not resolve from a folder that is not a configured load path.

I verified the tests fail for the right reasons by mutation: flipping the resolveBareSpecifiersAsPackages default to true fails exactly the default-behavior guard and nothing else, and reverting the resolver change fails exactly the four fix-targeting tests — reproducing Unexpected tilde in URL: ~plain-styles — while the guards stay green.

Because node_modules/ is gitignored, the package fixture tree is generated at test time under the project's temp/test/, so snapshots stay checkout-independent.

Docs

README options table plus a rewritten "Sass import resolution" section documenting the resolution order, why bare specifiers are not resolved as packages by default, and when to prefer pkg:. templates/sass.json gains commented entries for both options.

Since the move to the `pkg:` importer, a bare specifier such as
`@use '@scope/pkg/theme'` only ever resolved relative to the importing
file, so it failed with "Can't find stylesheet to import". There was no
configuration option to change this, and the failing import is often
inside a third-party package that the consuming project cannot edit.

- Bare specifiers now fall back to the new `loadPaths` option and then to
  `node_modules` when they do not resolve relative to the importing file.
  Relative resolution still takes precedence, matching Sass semantics.
- Package resolution failures return null instead of throwing, so Sass
  reports its usual error pointing at the offending line.
- The legacy `~` rewrite is applied in the resolver rather than only by
  the `@use`/`@import`/`@forward` preprocessor, so it also works in
  constructs such as `meta.load-css()`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b66c2e44-e9ac-4611-8c45-85cb60478f7b
@iclanton
Ian Clanton-Thuon (iclanton) force-pushed the fix-sass-bare-specifier-resolution branch from 47c1903 to d4e4123 Compare September 25, 2026 01:09
Comment thread heft-plugins/heft-sass-plugin/src/SassProcessor.ts
Addresses review feedback: per the Sass specification the target of
`@use`/`@import`/`@forward` is a URL, so `@use '@scope/pkg/theme'` is a
relative path and resolving it from node_modules is a deviation. That
deviation is now opt-in rather than automatic.

- Add `resolveBareSpecifiersAsPackages`, defaulting to false. Package
  resolution of a bare specifier happens only when it is enabled.
- `loadPaths` is likewise consulted only when configured.
- The legacy `~` rewrite is unchanged by this commit; `~` is an explicit
  package reference, so it is not gated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b66c2e44-e9ac-4611-8c45-85cb60478f7b
@iclanton Ian Clanton-Thuon (iclanton) changed the title [heft-sass-plugin] Restore resolution of bare specifiers [heft-sass-plugin] Add opt-in bare specifier resolution options Sep 28, 2026
@iclanton
Ian Clanton-Thuon (iclanton) merged commit 6cd8586 into microsoft:main Sep 28, 2026
7 checks passed
@iclanton
Ian Clanton-Thuon (iclanton) deleted the fix-sass-bare-specifier-resolution branch September 28, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Closed

Development

Successfully merging this pull request may close these issues.

3 participants