Skip to content

[heft-sass-plugin] Importer cannot resolve plain .css files referenced via @use / @import #5823

Description

@bartvandenende-wm

Summary

Since @rushstack/heft-sass-plugin v0.17.0, the plugin's Sass importer can no longer resolve a plain .css file referenced from another stylesheet via @use (or @import), even though the file exists on disk and dart-sass itself supports
loading plain CSS. Compilation fails with Can't find stylesheet to import.

This worked in v0.16.0 and earlier. It regressed in #5140 , which rewrote the importer. The loss of .css import resolution appears to be an unintended side-effect of the importer rewrite.

Repro steps

  1. In a project built with the latest @rushstack/heft-sass-plugin, add a plain CSS file, e.g. src/themes/tokens.css:
:root { --example: #fff; }
  1. Reference it from a .scss entry file via @use with an explicit .css extension:
    @use './themes/tokens.css' as tokens
  2. Run heft build

Expected result:
The .css file is located and loaded (parsed as plain CSS), matching dart-sass's support for @use/@import of plain CSS files — and matching the behavior of heft-sass-plugin <= 0.16.0.

Actual result:
Build fails:

[build:sass] Error: src/.../entry.scss:1:0 - Can't find stylesheet to import.
[build:sass] @use './themes/tokens.css' as tokens;

Details

In v0.16.0 the plugin used a findFileUrl FileImporter that only handled ~ package URLs and returned null for everything else, alongside loadPaths:

importers: [{
  findFileUrl: async (url, context) => {
    if (url[0] === '~') { /* resolve package */ }
    else { return null; } // defer to Sass's own filesystem resolver
  }
}],
loadPaths: importIncludePaths ?? [`${buildFolder}/node_modules`, `${buildFolder}/src`],

Returning null hands resolution back to Sass's native filesystem resolver, which resolves plain .css files. That is why @use '…css' worked.

In v0.17.0 (#5140) this was replaced with a custom canonicalize/load Importer using a heft: URL scheme, and importIncludePaths/loadPaths were removed. The heft: scheme is intentionally designed so "the SASS compiler will not try to load the resource itself", i.e. the custom importer now owns all resolution. But its candidate-extension logic only covers .sass/.scss:

https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-sass-plugin/src/SassProcessor.ts#L588-L603

private async _canonicalizeHeftInnerAsync(url: string, context: CanonicalizeContext): AsyncResolution {
  if (url.endsWith('.sass') || url.endsWith('.scss')) {
    // Extension is already present, so only try the exact URL or the corresponding partial
    return await this._canonicalizeFileAsync(url, context);
  }

  // Spec says prefer .sass, but we don't use that extension
  for (const candidate of [`${url}.scss`, `${url}.sass`, `${url}/index.scss`, `${url}/index.sass`]) {
    const result: SyncResolution = await this._canonicalizeFileAsync(candidate, context);
    if (result) {
      return result;
    }
  }

  return null;
}

For a URL ending in .css, the first branch is skipped and the loop instead probes tokens.css.scss, tokens.css.sass, tokens.css/index.scss, tokens.css/index.sass — none of which exist — so resolution returns null.

Note that .css is already in the default fileExtensions (['.sass', '.scss', '.css']), so the plugin happily compiles a .css entry file; it just can't resolve one referenced from inside another stylesheet.

Suggested fix: include .css in the exact-match branch so the literal file is attempted:

if (url.endsWith('.sass') || url.endsWith('.scss') || url.endsWith('.css')) {
  return await this._canonicalizeFileAsync(url, context);
}

Standard questions

Please answer these questions to help us investigate your issue more quickly:

Question Answer
@rushstack/heft version? 1.2.17 (@rushstack/heft-sass-plugin 1.4.1)
Operating system? Mac
Would you consider contributing a PR? yes
Node.js version (node -v)? 20.18.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Closed

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions