Skip to content

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

@bartvandenende-wm

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
    No fields configured for issues without a type.

    Projects

    Status
    Needs triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions