diff --git a/packages/core/integration-tests/test/sass.js b/packages/core/integration-tests/test/sass.js index ec642949dba..b3574588593 100644 --- a/packages/core/integration-tests/test/sass.js +++ b/packages/core/integration-tests/test/sass.js @@ -373,4 +373,30 @@ describe('sass', function () { }, ]); }); + + it('should support sass import resolution rules', async function () { + const dir = path.join(__dirname, 'sass-extensions'); + overlayFS.mkdirp(dir); + + await fsFixture(overlayFS, dir)` + index.js: + import './main.scss'; + + main.scss: + @use '~test' as test; + + node_modules/test/package.json: + { "name": "test" } + + node_modules/test/_index.scss: + @use 'other'; + + node_modules/test/_other.scss: + .foo { color: red } + `; + + await bundle(path.join(dir, '/index.js'), { + inputFS: overlayFS, + }); + }); }); diff --git a/packages/transformers/sass/src/modern.js b/packages/transformers/sass/src/modern.js index 10aaf04d36d..1f7bcdc81eb 100644 --- a/packages/transformers/sass/src/modern.js +++ b/packages/transformers/sass/src/modern.js @@ -95,7 +95,15 @@ function resolvePathImporter({ See also: https://github.com/sass/dart-sass/blob/006e6aa62f2417b5267ad5cdb5ba050226fab511/lib/src/importer/node/implementation.dart */ - let containingPath = fileURLToPath(containingUrl); + let containingPath = containingUrl + ? fileURLToPath(containingUrl) + : asset.filePath; + if (!containingUrl) { + // If containingUrl is not provided, then url should be an absolute file:/// URL. + let filePath = fileURLToPath(url); + url = path.relative(path.dirname(containingPath), filePath); + } + let paths = [path.dirname(containingPath)]; if (loadPaths) { paths.push(...loadPaths); @@ -110,17 +118,39 @@ function resolvePathImporter({ ); } + // The importer should look for stylesheets by adding the prefix _ to the URL's basename, + // and by adding the extensions .sass and .scss if the URL doesn't already have one of those extensions. const urls = [url]; const urlFileName = path.basename(url); if (urlFileName[0] !== '_') { - urls.push(path.join(path.dirname(url), `_${urlFileName}`)); + urls.push(path.posix.join(path.dirname(url), `_${urlFileName}`)); } + let ext = path.extname(urlFileName); + if (ext !== '.sass' && ext !== '.scss') { + for (let url of [...urls]) { + urls.push(url + '.sass'); + urls.push(url + '.scss'); + } + } + + // If none of the possible paths is valid, the importer should perform the same resolution on the URL followed by /index. + urls.push(path.posix.join(url, 'index.sass')); + urls.push(path.posix.join(url, 'index.scss')); + urls.push(path.posix.join(url, '_index.sass')); + urls.push(path.posix.join(url, '_index.scss')); + if (url[0] !== '~') { for (let p of paths) { for (let u of urls) { - const filePath = path.resolve(p, u); - if (await asset.fs.exists(filePath)) { + let filePath = path.resolve(p, u); + let stat; + try { + stat = await asset.fs.stat(filePath); + } catch (err) { + // ignore. + } + if (stat?.isFile()) { return pathToFileURL(filePath); }