What is the algorithm that Sass uses to resolve an @import statement?
Asked Answered
B

1

7

The algorithm that Node.js uses to resolve a require() call is very clearly documented in pseudocode.

I need the same thing for Sass's @import statement.

I know that @import 'foo' will try various combinations of the basenames foo and _foo, with extensions .scss and .sass, in the same directory as the importing file as well as relative to any of the configured 'load paths'... But what order these are tried in, i.e. what takes precedence if there are multiple files that could satisfy the @import? Does starting with a ./ or ../ affect whether it tries the load paths? Are there any other things it will try that I haven't covered? What about .css files?

The guide doesn't say much beyond "Sass is smart and will figure it out for you." The reference docs go into more detail, but still don't spell out the resolution steps.

Can anyone provide the exact algorithm it uses, in pseudocode?

Bedrabble answered 17/12, 2015 at 12:25 Comment(2)
You do realize that Sass is open source and that you could go find this out yourself, right?Tamatamable
@Tamatamable yep. Do you not think there's any value in it existing in pseudocode too?Bedrabble
L
6

Here is a simplified algorithm for @import <import_arg>;. This is derived from reading the source code for SASS and from running my own tests.

def main(import_arg)
  let dirname = File.dirname(import_arg)
  let basename = File.basename(import_arg)
  if import_arg is absolute ... # omitted as this is a rare case
  else return search(dirname, basename)
end

# try resolving import argument relative to each path in load_paths
# 1. If we encounter an unambiguous match, we finish
# 2. If we encounter an ambiguous match, we give up
# see: https://mcmap.net/q/205569/-what-does-gulp-39-s-includepaths-do
def search(dirname, basename)
  let cwd = operating system current working directory
  let load_paths = paths specified via SASS_PATH env variable and via --load-path options
  let search_paths = [cwd].concat(load_paths)
  for path in search_paths
    let file = find_match(File.expand_path(basename, path), basename)
    if (file != false) return file
  end
  throw "File to import not found or unreadable"
end

def find_match(directory, basename)
  let candidates = possible_files(directory, basename)
  if candiates.length == 0
    # not a single match found ... don't give up yet
    return false
  else if candidates.length > 1
    # several matching files, ambiguity! ... give up
    # throw ambiguity error
    throw "It's not clear which file to import"
  else
    # success! exactly one match found
    return candidates[0]
  end
end

# NB: this is a bit tricky to express in code
# which is why I settled for a high-level description
def possible_files(directory, basename)
  # if `basename` is of the form shown on the LHS
  # then check the filesystem for existence of
  # any of the files shown on the RHS within
  # directory `directory`. Return the list all the files
  # which do indeed exist (or [] if none exist).
  #   LHS        RHS
  #   x.sass -> _x.sass, x.sass
  #   x.scss -> _x.scss, x.scss
  #   x      -> x.scss, _x.scss, x.sass, _x.sass
  #   _x     -> _x.scss, _x.sass
end

Note for brevity, I am using Ruby's File#dirname, File#basename as well as File#expand which is like Node.js's path.resolve. I'm using a Ruby-like pseudocode but it is still meant to be pseudocode.

Key Points:

  • There is no precedence order. Rather than implementing a precedence order, SASS gives up when there are several possible candidates. For example, if you wrote @import "x" and say both x.scss and _x.scss exist, then sass will throw an ambiguity error. Similarly, if both x.scss and x.sass exist then an ambiguity error is thrown.
  • Load paths are tried from 'left to right' order. They provide a root or base to resolve imports from (similar how UNIX use $PATH to find executables). The current working directory is always tried first. (although this behaviour will change from 3.2 to 3.4)
  • Load paths are always tried regardless of whether you used ./ or ../
  • In a sass file, regular .css files cannot be imported

If you want more detail, I would recommend reading SASS's source code:

  • the import method of sass/lib/sass/tree/import_node.rb. On lines 53-56 you can see the same for loop as the one inside the search function in our pseudocode.
  • the Importers::Base abstract class sass/lib/sass/importors/base.rb. The comments of this file are quite handy.
  • the find_real_file method of sass/lib/sass/importors/filesystem.rb. Lines 112-126 implements our possible_files function. Line 156 checks there is only one match. If there isn't then line 167 throws an ambiguity error, else line 183 picks the one matching file.

Edit: I wasn't happy with my previous answer so I've rewritten it to be a bit clearer. Algorithm now correctly handles underscores in filename (previous algorithm didn't). I also added some key points that address the other questions OP asked.

Lyman answered 18/12, 2015 at 22:25 Comment(2)
The line let dirname = File.dirname(import_arg) confuses me. Don't you want the dirname of the importing file, not the file being imported?Bedrabble
Also, the dirname variable doesn't seem to be used anywhere. It's passed into search(), but that function ignores it.Bedrabble

© 2022 - 2024 — McMap. All rights reserved.