[Package Importer] Add types and specs (#3728)

Co-authored-by: Jonny Gerig Meyer <jonny@oddbird.net>
This commit is contained in:
James Stuckey Weber 2024-02-06 17:58:39 -05:00 committed by Natalie Weizenbaum
parent 804c6a6707
commit 336bfd69b2
15 changed files with 557 additions and 47 deletions

View File

@ -1,3 +1,8 @@
## 2.5.0
* Add `NodePackageImporter` as a built-in Package Importer, resolving `pkg:`
URLs using the standards and conventions of the Node ecosystem.
## 2.4.0
* Add `CompileRequest.silent` option that suppresses all `LogEvent`s.

View File

@ -1,3 +1,10 @@
## Draft 1.6
* Change from an entry point path to entry point directory.
* Add a private readonly symbol key to `NodePackageImporter` type to
distinguish it from arbitrary objects.
## Draft 1.5
* Specify that Package Importers must not reject URL patterns that other Package

View File

@ -1,11 +1,10 @@
# Package Importer: Draft 1.5
# Package Importer: Draft 1.6
*([Issue](https://github.com/sass/sass/issues/2739))*
This proposal introduces the semantics for a Package Importer and defines the
`pkg:` URL scheme to indicate Sass package imports in an implementation-agnostic
format. It also defines the semantics for a new built-in Node Package
Importer.
format. It also defines the semantics for a new built-in Node Package Importer.
## Table of Contents
@ -123,9 +122,9 @@ the reader where to find the file.
While the Dart Sass implementation allows for the use of the `package:` URL
scheme, a similar standard doesn't exist in Node. We chose the `pkg:` URL scheme
as it clearly communicates to both the user and compiler that the specified files
are from a dependency. The `pkg:` URL scheme also does not have known conflicts
in the ecosystem.
as it clearly communicates to both the user and compiler that the specified
files are from a dependency. The `pkg:` URL scheme also does not have known
conflicts in the ecosystem.
#### No built-in `pkg:` resolver for browsers
@ -188,13 +187,14 @@ use the CSS exports exposed through `"style"`. While in practice, `"style"`
tends to be used solely for `css` files, we will support `scss`, `sass` and
`css` files for either `"sass"` or `"style"`.
While conditional exports allows package authors to define specific aliases to internal
files, we will still use the Sass conventions for resolving file paths with
partials, extensions and indices to discover the intended export alias. However,
we will not apply that logic to the destination, and will expect library authors
to map the export to the correct place. In other words, given a `package.json`
with `exports` as below, The Node package importer will resolve a
`@use "pkg:pkgName/variables";` to the destination of the `_variables.scss` export.
While conditional exports allows package authors to define specific aliases to
internal files, we will still use the Sass conventions for resolving file paths
with partials, extensions and indices to discover the intended export alias.
However, we will not apply that logic to the destination, and will expect
library authors to map the export to the correct place. In other words, given a
`package.json` with `exports` as below, The Node package importer will resolve a
`@use "pkg:pkgName/variables";` to the destination of the `_variables.scss`
export.
```json
{
@ -223,7 +223,8 @@ when compiling a string like `compileString('@use "pkg:bootstrap";')`, we don't
know where to start looking for the Bootstrap module. We considered
`require.main.filename` and the current working directory, but found that
neither would allow for all use cases. We decided to allow users to specify an
entry point path, defaulting to `require.main.filename`.
entry point directory, defaulting to the parent directory of
`require.main.filename`.
## Types
@ -234,8 +235,13 @@ import {FileImporter, Importer} from '../spec/js-api/importer';
### `NodePackageImporter`
```ts
declare const nodePackageImporterKey: unique symbol;
export class NodePackageImporter {
constructor(entryPointPath?: string);
/** Used to distinguish this type from any arbitrary object. */
private readonly [nodePackageImporterKey]: true;
constructor(entryPointDirectory?: string);
}
```
@ -269,13 +275,13 @@ Javascript Compile API, insert:
> This primarily refers to a browser environment, but applies to other
> sandboxed JavaScript environments as well.
* Let `entryPointPath` be the class's `entryPointPath` value if set, resolved
relative to the current working directory, and otherwise the value of
`require.main.filename`. If `entryPointPath` is not passed and
`require.main.filename` is not available, throw an error.
* Let `entryPointDirectory` be the class's `entryPointDirectory` value if set,
resolved relative to the current working directory, and otherwise the parent
directory of `require.main.filename`. If `entryPointDirectory` is not passed
and `require.main.filename` is not available, throw an error.
* Let `pkgImporter` be a [Node Package Importer] with an associated
`entryPointURL` of the absolute file URL for`entryPointPath`.
`entryPointURL` of the absolute file URL for `entryPointDirectory`.
* Replace the item with `pkgImporter` in a copy of `options.importers`.
@ -290,19 +296,22 @@ any URL with the `pkg:` scheme. This step will be inserted immediately before
the existing legacy importer logic, and if the package importer returns `null`,
the legacy importer logic will be invoked.
Currently, the only available package importer is `node`, which follows Node
resolution logic to locate Sass files.
Currently, the only available package importer is `NodePackageImporter`, which
follows Node resolution logic to locate Sass files.
An optional `entryPointPath` path can be passed to provide a starting
`parentURL` for the Node package resolution algorithm. If not set, the default
value is `require.main.filename`.
An optional `entryPointDirectory` path can be passed to the
`NodePackageImporter` to provide a starting `parentURL` for the Node package
resolution algorithm. If not set, the default value is the parent directory of
`require.main.filename`.
Defaults to undefined.
```ts
import {NodePackageImporter as BaseNodePackageImporter} from '../spec/js-api/importer';
declare module '../spec/js-api/legacy/options' {
export interface LegacySharedOptions<sync extends 'sync' | 'async'> {
pkgImporter?: NodePackageImporter;
pkgImporter?: BaseNodePackageImporter;
}
}
```
@ -313,7 +322,8 @@ declare module '../spec/js-api/legacy/options' {
This proposal defines the requirements for Package Importers written by users or
provided by implementations. It is a type of [Importer] and, in addition to the
standard requirements for importers, it must handle only non-canonical URLs that:
standard requirements for importers, it must handle only non-canonical URLs
that:
* have the scheme `pkg`, and
* whose path begins with a package name, and
@ -365,8 +375,8 @@ When the Node Package Importer is invoked with a string named `string`:
* Otherwise, let `baseURL` be `entryPointURL`.
* Let `resolved` be the result of [resolving a `pkg:` URL as Node] with `url` and
`baseURL`.
* Let `resolved` be the result of [resolving a `pkg:` URL as Node] with `url`
and `baseURL`.
* If `resolved` is null, return null.
@ -436,8 +446,8 @@ It returns a canonical `file:` URL or null.
This algorithm takes a string, `path`, and returns the portion that identifies
the Node package.
* If `path` starts with `@`, it is a scoped package. Return the first 2 [URL path
segments], including the separating `/`.
* If `path` starts with `@`, it is a scoped package. Return the first 2 [URL
path segments], including the separating `/`.
* Otherwise, return the first URL path segment.
@ -447,9 +457,13 @@ This algorithm takes a string, `packageName`, and an absolute URL `baseURL`, and
returns an absolute URL to the root directory for the most proximate installed
`packageName`.
* Return the result of `PACKAGE_RESOLVE(packageName, baseURL)` as defined in
the [Node resolution algorithm specification].
* Let `baseDirectory` be `baseURL` appended with a [single-dot URL path
segment].
* Return the result of `PACKAGE_RESOLVE(packageName, baseDirectory)` as defined
in the [Node resolution algorithm specification].
[single-dot URL path segment]: https://url.spec.whatwg.org/#single-dot-path-segment
[Node resolution algorithm specification]: https://nodejs.org/api/esm.html#resolution-algorithm-specification
### Resolving package exports
@ -481,7 +495,8 @@ This algorithm takes a package.json value `packageManifest`, a directory URL
* Let `subpathIndex` be `subpath` + `"/index"`.
* Let `subpathIndexVariants` be the result of [Export load paths] with `subpathIndex`.
* Let `subpathIndexVariants` be the result of [Export load paths] with
`subpathIndex`.
* Let `resolvedIndexPaths` be a list of the results of calling
`PACKAGE_EXPORTS_RESOLVE(packageRoot, subpathVariant, exports, ["sass",
@ -552,12 +567,12 @@ potential subpaths, resolving for partials and file extensions.
## Embedded Protocol
An Importer that resolves `pkg:` URLs using the [Node resolution algorithm]. It
is instantiated with an associated `entry_point_path`, which is either absolute
or will be resolved relative to the current working directory.
is instantiated with an associated `entry_point_directory`, which is either
absolute or will be resolved relative to the current working directory.
```proto
message NodePackageImporter {
string entry_point_path = 1;
string entry_point_directory = 1;
}
```

View File

@ -308,6 +308,139 @@ export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
nonCanonicalScheme?: string | string[];
}
declare const nodePackageImporterKey: unique symbol;
/**
* The built-in Node.js package importer. This loads pkg: URLs from node_modules
* according to the standard Node.js resolution algorithm.
*
* A Node.js package importer is exposed as a class that can be added to the
* `importers` option.
*
*```js
* const sass = require('sass');
* sass.compileString('@use "pkg:vuetify', {
* importers: [new sass.NodePackageImporter()]
* });
*```
*
* ## Writing Sass packages
*
* Package authors can control what is exposed to their users through their
* `package.json` manifest. The recommended method is to add a `sass`
* conditional export to `package.json`.
*
* ```json
* // node_modules/uicomponents/package.json
* {
* "exports": {
* ".": {
* "sass": "./src/scss/index.scss",
* "import": "./dist/js/index.mjs",
* "default": "./dist/js/index.js"
* }
* }
* }
* ```
*
* This allows a package user to write `@use "pkg:uicomponents"` to load the
* file at `node_modules/uicomponents/src/scss/index.scss`.
*
* The Node.js package importer supports the variety of formats supported by
* Node.js [package entry points], allowing authors to expose multiple subpaths.
*
* [package entry points]:
* https://nodejs.org/api/packages.html#package-entry-points
*
* ```json
* // node_modules/uicomponents/package.json
* {
* "exports": {
* ".": {
* "sass": "./src/scss/index.scss",
* },
* "./colors": {
* "sass": "./src/scss/_colors.scss",
* },
* "./theme/*": {
* "sass": "./src/scss/theme/*.scss",
* },
* }
* }
* ```
*
* This allows a package user to write:
*
* - `@use "pkg:uicomponents";` to import the root export.
* - `@use "pkg:uicomponents/colors";` to import the colors partial.
* - `@use "pkg:uicomponents/theme/purple";` to import a purple theme.
*
* Note that while library users can rely on the importer to resolve
* [partials](https://sass-lang.com/documentation/at-rules/use#partials), [index
* files](https://sass-lang.com/documentation/at-rules/use#index-files), and
* extensions, library authors must specify the entire file path in `exports`.
*
* In addition to the `sass` condition, the `style` condition is also
* acceptable. Sass will match the `default` condition if it's a relevant file
* type, but authors are discouraged from relying on this. Notably, the key
* order matters, and the importer will resolve to the first value with a key
* that is `sass`, `style`, or `default`, so you should always put `default`
* last.
*
* To help package authors who haven't transitioned to package entry points
* using the `exports` field, the Node.js package importer provides several
* fallback options. If the `pkg:` URL does not have a subpath, the Node.js
* package importer will look for a `sass` or `style` key at the root of
* `package.json`.
*
* ```json
* // node_modules/uicomponents/package.json
* {
* "sass": "./src/scss/index.scss",
* }
* ```
*
* This allows a user to write `@use "pkg:uicomponents";` to import the
* `index.scss` file.
*
* Finally, the Node.js package importer will look for an `index` file at the
* package root, resolving partials and extensions. For example, if the file
* `_index.scss` exists in the package root of `uicomponents`, a user can import
* that with `@use "pkg:uicomponents";`.
*
* If a `pkg:` URL includes a subpath that doesn't have a match in package entry
* points, the Node.js importer will attempt to find that file relative to the
* package root, resolving for file extensions, partials and index files. For
* example, if the file `src/sass/_colors.scss` exists in the `uicomponents`
* package, a user can import that file using `@use
* "pkg:uicomponents/src/sass/colors";`.
*
* @compatibility dart: "2.0", node: false
* @category Importer
*/
export class NodePackageImporter {
/** Used to distinguish this type from any arbitrary object. */
private readonly [nodePackageImporterKey]: true;
/**
* The NodePackageImporter has an optional `entryPointDirectory` option, which
* is the directory where the Node Package Importer should start when
* resolving `pkg:` URLs in sources other than files on disk. This will be
* used as the `parentURL` in the [Node Module
* Resolution](https://nodejs.org/api/esm.html#resolution-algorithm-specification)
* algorithm.
*
* In order to be found by the Node Package Importer, a package will need to
* be inside a node_modules folder located in the `entryPointDirectory`, or
* one of its parent directories, up to the filesystem root.
*
* Relative paths will be resolved relative to the current working directory.
* If a path is not provided, the default value of `require.main.filename`
* will be used.
*/
constructor(entryPointDirectory?: string);
}
/**
* The result of successfully loading a stylesheet with an {@link Importer}.
*

View File

@ -19,6 +19,7 @@ export {
FileImporter,
Importer,
ImporterResult,
NodePackageImporter,
} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {

View File

@ -1,6 +1,7 @@
import {Logger} from '../logger';
import {LegacyImporter} from './importer';
import {LegacyFunction} from './function';
import {NodePackageImporter} from '../importer';
/**
* Options for {@link render} and {@link renderSync} that are shared between
@ -508,6 +509,24 @@ export interface LegacySharedOptions<sync extends 'sync' | 'async'> {
* @compatibility dart: "1.43.0", node: false
*/
logger?: Logger;
/**
* If this option is set to an instance of `NodePackageImporter`, Sass will
* use the built-in Node.js package importer to resolve Sass files with a
* `pkg:` URL scheme. Details for library authors and users can be found in
* the {@link NodePackageImporter} documentation.
*
* @example
* ```js
* sass.renderSync({
* data: '@use "pkg:vuetify";',
* pkgImporter: new sass.NodePackageImporter()
* });
* ```
* @category Plugins
* @compatibility dart: "2.0", node: false
*/
pkgImporter?: NodePackageImporter;
}
/**

View File

@ -1,4 +1,4 @@
import {FileImporter, Importer} from './importer';
import {FileImporter, Importer, NodePackageImporter} from './importer';
import {Logger} from './logger';
import {Value} from './value';
import {PromiseOr} from './util/promise_or';
@ -208,8 +208,8 @@ export interface Options<sync extends 'sync' | 'async'> {
* - The importer that was used to load the current stylesheet, with the
* loaded URL resolved relative to the current stylesheet's canonical URL.
*
* - Each {@link Importer} or {@link FileImporter} in {@link importers}, in
* order.
* - Each {@link Importer}, {@link FileImporter}, or
* {@link NodePackageImporter} in {@link importers}, in order.
*
* - Each load path in {@link loadPaths}, in order.
*
@ -218,7 +218,7 @@ export interface Options<sync extends 'sync' | 'async'> {
*
* @category Plugins
*/
importers?: (Importer<sync> | FileImporter<sync>)[];
importers?: (Importer<sync> | FileImporter<sync> | NodePackageImporter)[];
/**
* Paths in which to look for stylesheets loaded by rules like

View File

@ -1 +1 @@
2.4.0
2.5.0

View File

@ -98,6 +98,13 @@ message InboundMessage {
// generating this ID and ensuring that it's unique across all importers
// registered for this compilation.
uint32 file_importer_id = 3;
// The [Node.js package importer], which is a built-in Package Importer
// with an associated `entry_point_directory` that resolves `pkg:` URLs
// using the standards and conventions of the Node ecosystem.
//
// [Node.js package importer]: https://github.com/sass/sass/tree/main/spec/modules.md#node-package-importer
NodePackageImporter node_package_importer = 5;
}
// The set of URL schemes that are considered *non-canonical* for this
@ -1063,3 +1070,16 @@ enum CalculationOperator {
// The division operator.
DIVIDE = 3;
}
// The built-in Node.js Package Importer, which is a Package Importer that
// resolves using the standards and conventions of the Node.js ecosystem. It
// enables a `pkg:` URL scheme for usage with `@use` that directs an
// implementation to resolve a URL within a dependency.
message NodePackageImporter {
// The absolute path to associate with the Node Package Importer, with
// semantics identical to the [entryPointDirectory option] in the JavaScript
// API.
//
// [entryPointDirectory option]: https://sass-lang.com/documentation/js-api/classes/NodePackageImporter.html#constructor
string entry_point_directory = 1;
}

View File

@ -126,6 +126,26 @@ When `dispose` is invoked on an Async Compiler:
Compiles the Sass file at `path`:
* If any item in `options.importers` is an instance of the `NodePackageImporter`
class:
* If no filesystem is available, throw an error.
> This primarily refers to a browser environment, but applies to other
> sandboxed JavaScript environments as well.
* Let `entryPointDirectory` be the class's `entryPointDirectory` value if set,
resolved relative to the current working directory, and otherwise the parent
directory of `require.main.filename`. If `entryPointDirectory` is not passed
and `require.main.filename` is not available, throw an error.
* Let `pkgImporter` be a [Node Package Importer] with an associated
`entryPointURL` of the absolute file URL for`entryPointDirectory`.
[Node Package Importer]: ../modules.md#node-package-importer
* Replace the item with `pkgImporter` in a copy of `options.importers`.
* If any object in `options.importers` has both `findFileUrl` and `canonicalize`
fields, throw an error.
@ -177,6 +197,24 @@ export function compileAsync(
Compiles the Sass `source`:
* If any item in `options.importers` is an instance of the `NodePackageImporter`
class:
* If no filesystem is available, throw an error.
> This primarily refers to a browser environment, but applies to other
> sandboxed JavaScript environments as well.
* Let `entryPointDirectory` be the class's `entryPointDirectory` value if set,
resolved relative to the current working directory, and otherwise the parent
directory of `require.main.filename`. If `entryPointDirectory` is not passed
and `require.main.filename` is not available, throw an error.
* Let `pkgImporter` be a [Node Package Importer] with an associated
`entryPointURL` of the absolute file URL for`entryPointDirectory`.
* Replace the item with `pkgImporter` in a copy of `options.importers`.
* If `options.importer` or any object in `options.importers` has both
`findFileUrl` and `canonicalize` fields, throw an error.

View File

@ -15,6 +15,7 @@ import {PromiseOr} from './util/promise_or';
* [`FileImporter`](#fileimporter)
* [`Importer`](#importer)
* [`nonCanonicalScheme`](#noncanonicalscheme)
* [`NodePackageImporter`](#nodepackageimporter)
* [`ImporterResult`](#importerresult)
## Types
@ -161,6 +162,19 @@ nonCanonicalScheme?: string | string[];
} // Importer
```
### `NodePackageImporter`
```ts
declare const nodePackageImporterKey: unique symbol;
export class NodePackageImporter {
/** Used to distinguish this type from any arbitrary object. */
private readonly [nodePackageImporterKey]: true;
constructor(entryPointDirectory?: string);
}
```
### `ImporterResult`
```ts

View File

@ -56,6 +56,7 @@ export {
FileImporter,
Importer,
ImporterResult,
NodePackageImporter,
} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {

View File

@ -4,6 +4,7 @@
import {Logger} from '../logger';
import {LegacyImporter} from './importer';
import {LegacyFunction} from './function';
import {NodePackageImporter} from '../importer';
```
## Table of Contents
@ -27,6 +28,7 @@ import {LegacyFunction} from './function';
* [`quietDeps`](#quietdeps)
* [`verbose`](#verbose)
* [`logger`](#logger)
* [`pkgImporter`](#pkgimporter)
* [`LegacyFileOptions`](#legacyfileoptions)
* [`LegacyStringOptions`](#legacystringoptions)
* [`LegacyOptions`](#legacyoptions)
@ -173,6 +175,19 @@ doesn't have `warn` or `debug` fields.
logger?: Logger;
```
#### `pkgImporter`
If set to an instance of [NodePackageImporter], the compiler will use it as the
specified built-in package importer to resolve any URL with the `pkg:` scheme.
This importer will be checked immediately before the existing legacy importer
logic, and if it returns `null`, the legacy importer logic will be invoked.
[NodePackageImporter]: ../importer.d.ts.md#nodepackageimporter
```ts
pkgImporter?: NodePackageImporter;
```
```ts
} // LegacySharedOptions
```

View File

@ -6,7 +6,7 @@
> [compile API]: compile.d.ts.md
```ts
import {FileImporter, Importer} from './importer';
import {FileImporter, Importer, NodePackageImporter} from './importer';
import {Logger} from './logger';
import {Value} from './value';
import {PromiseOr} from './util/promise_or';
@ -165,12 +165,14 @@ functions?: Record<string, CustomFunction<sync>>;
#### `importers`
The list of [custom importers] to use to resolve file loads.
The list of [custom importers] or [package importers] to use to resolve file
loads.
[custom importers]: importer.d.ts.md
[package importers]: importer.d.ts.md
```ts
importers?: (Importer<sync> | FileImporter<sync>)[];
importers?: (Importer<sync> | FileImporter<sync> | NodePackageImporter)[];
```
#### `loadPaths`

View File

@ -12,6 +12,8 @@
* [Built-In Module](#built-in-module)
* [Importer](#importer)
* [Filesystem Importer](#filesystem-importer)
* [Package Importers](#package-importers)
* [Node Package Importer](#node-package-importer)
* [Global Importer List](#global-importer-list)
* [Basename](#basename)
* [Dirname](#dirname)
@ -23,6 +25,13 @@
* [Resolving a `file:` URL for Extensions](#resolving-a-file-url-for-extensions)
* [Resolving a `file:` URL for Partials](#resolving-a-file-url-for-partials)
* [Resolving a Member](#resolving-a-member)
* [Node Package Importer Semantics](#node-package-importer-semantics)
* [Node Algorithm for Resolving a `pkg:` URL](#node-algorithm-for-resolving-a-pkg-url)
* [Resolving a package name](#resolving-a-package-name)
* [Resolving the root directory for a package](#resolving-the-root-directory-for-a-package)
* [Resolving package exports](#resolving-package-exports)
* [Resolving package root values](#resolving-package-root-values)
* [Export Load Paths](#export-load-paths)
## Definitions
@ -211,6 +220,81 @@ named `string`:
[parsing a URL]: https://url.spec.whatwg.org/#concept-url-parser
### Package Importers
This proposal defines the requirements for Package Importers written by users or
provided by implementations. It is a type of [Importer] and, in addition to the
standard requirements for importers, it must handle only non-canonical URLs that:
* have the scheme `pkg`, and
* whose path begins with a package name, and
* optionally followed by a path, with path segments separated with a forward
slash.
The package name will often be the first path segment, but the importer may take
into account any conventions in the environment. For instance, Node supports
scoped package names, which start with `@` followed by 2 path segments. Note
that package names that contain non-alphanumeric characters may be less portable
across different package importers.
Package Importers must reject the following patterns:
* A URL whose path begins with `/`.
* A URL with non-empty/null username, password, host, port, query, or fragment.
[importer]: #importer
### Node Package Importer
The Node Package Importer is an implementation of a [Package Importer] using the
standards and conventions of the Node ecosystem. It has an associated absolute
`file:` URL named `entryPointURL`.
When the Node Package Importer is invoked with a string named `string`:
* If `string` is a relative URL, return null.
* Let `url` be the result of [parsing `string` as a URL][parsing a URL]. If this
returns a failure, throw that failure.
* If `url`'s scheme is not `pkg:`, return null.
* If `url`'s path begins with a `/` or is empty, throw an error.
* If `url` contains a username, password, host, port, query, or fragment, throw
an error.
* Let `sourceFile` be the canonical URL of the [current source file] that
contained the load.
* If `sourceFile`'s scheme is `file:`, let `baseURL` be `sourceFile`.
* Otherwise, let `baseURL` be `entryPointURL`.
* Let `resolved` be the result of [resolving a `pkg:` URL as Node] with `url` and
`baseURL`.
* If `resolved` is null, return null.
* Let `text` be the contents of the file at `resolved`.
* Let `syntax` be:
* "scss" if `resolved` ends in `.scss`.
* "indented" if `resolved` ends in `.sass`.
* "css" if `resolved` ends in `.css`.
> The algorithm for [resolving a `pkg:` URL as Node] guarantees that
> `resolved` will have one of these extensions.
* Return `text`, `syntax`, and `resolved`.
[Package Importer]: #package-importers
[current source file]: ./spec.md#current-source-file
[resolving a `pkg:` URL as Node]: #node-algorithm-for-resolving-a-pkg-url
### Global Importer List
The *global importer list* is a list of importers that's set for the entire
@ -322,7 +406,6 @@ null.
* Return null.
[current source file]: spec.md#current-source-file
[parsing]: syntax.md#parsing-text
### Resolving a `file:` URL
@ -506,3 +589,160 @@ and returns a member of type `type` or null.
> implementation's host language API.
* Otherwise, return null.
### Node Package Importer Semantics
#### Node Algorithm for Resolving a `pkg:` URL
This algorithm takes a URL with scheme `pkg:` named `url`, and a URL `baseURL`.
It returns a canonical `file:` URL or null.
* Let `fullPath` be `url`'s path.
* Let `packageName` be the result of [resolving a package name] with `fullPath`,
and `subpath` be `fullPath` without the `packageName`.
* Let `packageRoot` be the result of [resolving the root directory for a
package] with `packageName` and `baseURL`.
* If a `package.json` file does not exist at `packageRoot`, throw an error.
* Let `packageManifest` be the result of parsing the `package.json` file at
`packageRoot` as [JSON].
* Let `resolved` be the result of [resolving package exports] with
`packageRoot`, `subpath`, and `packageManifest`.
* If `resolved` has the scheme `file:` and an extension of `sass`, `scss` or
`css`, return it.
* Otherwise, if `resolved` is not null, throw an error.
* If `subpath` is empty, return the result of [resolving package root values].
* Let `resolved` be `subpath` resolved relative to `packageRoot`.
* Return the result of [resolving a `file:` URL] with `resolved`.
[Resolving package exports]: #resolving-package-exports
[resolving package root values]: #resolving-package-root-values
[resolving a package name]: #resolving-a-package-name
[JSON]: https://datatracker.ietf.org/doc/html/rfc8259
[resolving the root directory for a package]: #resolving-the-root-directory-for-a-package
[resolving a `file:` URL]: #resolving-a-file-url
#### Resolving a package name
This algorithm takes a string, `path`, and returns the portion that identifies
the Node package.
* If `path` starts with `@`, it is a scoped package. Return the first 2 [URL path
segments], including the separating `/`.
* Otherwise, return the first URL path segment.
#### Resolving the root directory for a package
This algorithm takes a string, `packageName`, and an absolute URL `baseURL`, and
returns an absolute URL to the root directory for the most proximate installed
`packageName`.
* Return the result of `PACKAGE_RESOLVE(packageName, baseURL)` as defined in
the [Node resolution algorithm specification].
[Node resolution algorithm specification]: https://nodejs.org/api/esm.html#resolution-algorithm-specification
#### Resolving package exports
This algorithm takes a package.json value `packageManifest`, a directory URL
`packageRoot` and a relative URL path `subpath`. It returns a file URL or null.
* Let `exports` be the value of `packageManifest.exports`.
* If `exports` is undefined, return null.
* Let `subpathVariants` be the result of [Export load paths] with `subpath`.
* Let `resolvedPaths` be a list of the results of calling
`PACKAGE_EXPORTS_RESOLVE(packageRoot, subpathVariant, exports, ["sass",
"style"])` as defined in the [Node resolution algorithm specification], with
each `subpathVariants` as `subpathVariant`.
> The PACKAGE_EXPORTS_RESOLVE algorithm always includes a `default` condition,
> so one does not have to be passed here.
* If `resolvedPaths` contains more than one resolved URL, throw an error.
* If `resolvedPaths` contains exactly one resolved URL, return it.
* If `subpath` has an extension, return null.
* Let `subpathIndex` be `subpath` + `"/index"`.
* Let `subpathIndexVariants` be the result of [Export load paths] with `subpathIndex`.
* Let `resolvedIndexPaths` be a list of the results of calling
`PACKAGE_EXPORTS_RESOLVE(packageRoot, subpathVariant, exports, ["sass",
"style"])` as defined in the [Node resolution algorithm specification], with
each `subpathIndexVariants` as `subpathVariant`.
* If `resolvedIndexPaths` contains more than one resolved URL, throw an error.
* If `resolvedIndexPaths` contains exactly one resolved URL, return it.
* Return null.
> Where possible in Node, implementations can use [resolve.exports] which
> exposes the Node resolution algorithm, allowing for per-path custom
> conditions, and without needing filesystem access.
[Export load paths]: #export-load-paths
[resolve.exports]: https://github.com/lukeed/resolve.exports
#### Resolving package root values
This algorithm takes a string `packagePath`, which is the root directory for a
package, and `packageManifest`, which is the contents of that package's
`package.json` file, and returns a file URL.
* Let `sassValue` be the value of `sass` in `packageManifest`.
* If `sassValue` is a relative path with an extension of `sass`, `scss` or
`css`:
* Return the canonicalized `file:` URL for `${packagePath}/${sassValue}`.
* Let `styleValue` be the value of `style` in `packageManifest`.
* If `styleValue` is a relative path with an extension of `sass`, `scss` or
`css`:
* Return the canonicalized `file:` URL for `${packagePath}/${styleValue}`.
* Otherwise return the result of [resolving a `file:` URL for extensions] with
`packagePath + "/index"`.
[resolving a `file:` URL for extensions]: #resolving-a-file-url-for-extensions
[URL path segments]: https://url.spec.whatwg.org/#url-path-segment
#### Export Load Paths
This algorithm takes a relative URL path `subpath` and returns a list of
potential subpaths, resolving for partials and file extensions.
* Let `paths` be a list.
* If `subpath` ends in `.scss`, `.sass`, or `.css`:
* Add `subpath` to `paths`.
* Otherwise, add `subpath` + `.scss`, `subpath` + `.sass`, and `subpath` +
`.css` to `paths`.
* If `subpath`'s [basename] does not start with `_`, for each `item` in
`paths`, prepend `"_"` to the basename, and add to `paths`.
* Return `paths`.
[basename]: #basename