Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update documentation and blog for pkg: importers #944

Merged
merged 6 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions source/blog/040-announcing-pkg-importers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
title: "Announcing `pkg:` Importers"
author: Natalie Weizenbaum
date: 2024-02-06 15:30:00 -8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update date to reflect the date this is actually posted.

---

Several months ago, we [asked for feedback] on a proposal for a new standard for
importers that could load packages from various different package managers using
the shared `pkg:` scheme, as well as a built-in `pkg:` importer that supports
Node.js's module resolution algorithm. Today, I'm excited to announce that this
feature has shipped in Dart Sass 1.71.0!

[asked for feedback]: /blog/rfc-package-importer

No longer will you have to manually add `node_modules` to your `loadPaths`
option and worry about whether nested packages will work at all. No longer will
you need to add `~`s to your URLs and give up all portability. Now you can just
pass `importers: [new NodePackageImporter()]` and write `@use 'pkg:library'` and
it'll work just how you want out of the box.

## What is a `pkg:` importer?

Think of a `pkg:` importer like a specification that anyone can implement by
writing a [custom importer] that follows [a few rules]. We've implemented one for
the Node.js module algorithm, but you could implement one that loads Sass files
from [RubyGems] or [PyPI] or [Composer]. This way, a Sass file doesn't have to
change the URLs it loads no matter where it's loading them from.

[custom importer]: /documentation/js-api/interfaces/Options/#importers
[a few rules]: /documentation/at-rules/use#rules-for-a-pkg-importer
[RubyGems]: https://rubygems.org/
[PyPI]: https://pypi.org/
[Composer]: https://getcomposer.org/

## What do `pkg:` URLs look like?

The simplest URL is just `pkg:library`. This will find the `library` package in
your package manager and load its primary entrypoint file, however that's
defined. You can also write `pkg:library/path/to/file`, in which case it will
look for `path/to/file` in the package's source directory instead. And as with
any Sass importer, it'll do the standard resolution to handle file extensions,
[partials], and [index files].

[partials]: /documentation/at-rules/use#partials
[index files]: /documentation/at-rules/use#index-files

## How do I publish an npm package that works with the Node.js `pkg:` importer?

The Node.js `pkg:` importer supports all the existing conventions for declaring
Sass files in `package.json`, so it should work with existing Sass packages out
of the box. If you're writing a new package, we recommend using the [`"exports"`
field] with a `"sass"` key to define which stylesheet to load by default:

[`"exports"` field]: https://nodejs.org/api/packages.html#conditional-exports

```json
{
"exports": {
"sass": "styles/index.scss"
}
}
```

The Node.js `pkg:` importer supports the full range of `"exports"` features, so
you can also specify different locations for different subpaths:

```json
{
"exports": {
".": {
"sass": "styles/index.scss",
},
"./button.scss": {
"sass": "styles/button.scss",
},
"./accordion.scss": {
"sass": "styles/accordion.scss",
}
}
}
```

...or even patterns:

```json
{
"exports": {
".": {
"sass": "styles/index.scss",
},
"./*.scss": {
"sass": "styles/*.scss",
},
}
}
```
137 changes: 136 additions & 1 deletion source/documentation/at-rules/use.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,10 @@ load; `@use "variables"` will automatically load `variables.scss`,
All Sass implementations allow users to provide *load paths*: paths on the
filesystem that Sass will look in when locating modules. For example, if you
pass `node_modules/susy/sass` as a load path, you can use `@use "susy"` to load
`node_modules/susy/sass/susy.scss`.
`node_modules/susy/sass/susy.scss` (although [`pkg:` URLs] are a better way to
handle that).

[`pkg:` URLs]: #pkg-ur-ls

Modules will always be loaded relative to the current file first, though. Load
paths will only be used if no relative file exists that matches the module's
Expand Down Expand Up @@ -522,6 +525,138 @@ be loaded automatically when you load the URL for the folder itself.
}
{% endcodeExample %}

## `pkg:` URLs

Sass uses the `pkg:` URL scheme to load stylesheets distributed by various
package managers. Since Sass is used in the context of many different
programming languages with different package management conventions, `pkg:` URLs
have almost no set meaning. Instead, users are encouraged to implement custom
importers (using the [JS API] or the [Embedded Sass protocol]) that resolve
these URLs using the native package manager's logic.

This allows `pkg:` URLs and the stylesheets that use them to be portable across
different language ecosystems. Whether you're installing a Sass library via npm
(for which Sass provides [a built-in `pkg:` importer]) or the most obscure
package manager you can find, if you write `@use 'pkg:library'` it'll do the
right thing.

[JS API]: /documentation/js-api/interfaces/Options/#importers
[Embedded Sass protocol]: https://github.com/sass/sass/blob/main/spec/embedded-protocol.md
[a built-in `pkg:` importer]: #node-js-package-importer

{% funFact %}
`pkg:` URLs aren't just for `@use`. You can use them anywhere you can load a
Sass file, including [`@forward`], [`meta.load-css()`], and even the old
[`@import`] rule.

[`@forward`]: /documentation/at-rules/forward
[`meta.load-css()`]: /documentation/modules/meta/#load-css
[`@import`]: /documentation/at-rules/import
{% endfunFact %}

### Rules for a `pkg:` Importer

There are a few common rules that Sass expects all `pkg:` importers to follow.
These rules help ensure that `pkg:` URLs are handled consistently across all
package managers, so that stylesheets are as portable as possible.

In addition to the standard rules for custom importers, a `pkg:` importer must
only handle non-canonical URLs that:

* have the scheme `pkg`, and
* whose path begins with a package name, and
* are optionally followed by a path, with path segments separated with a forward
slash.

The package name may contain forward slashes, depending on whether the
particular package manager supports that. For example, npm allows package names
like `@namespace/name`. Note that package names that contain non-alphanumeric
characters may be less portable across different package managers.

`pkg:` 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.

If `pkg:` importer encounters a URL that violates its own package manager's
conventions but _not_ the above rules, it should just decline to load that URL
rather than throwing an error. This allows users to use multiple `pkg:`
importers at once if necessary.

### Node.js Package Importer

{% compatibility 'dart: "1.71.0"', 'libsass: false', 'ruby: false' %}{% endcompatibility %}

Because Sass is most widely-used alongside the Node.js ecosystem, it comes with
a `pkg:` importer that uses the same algorithm as Node.js to load Sass
stylesheets. This isn't available by default, but it's easy to turn on:

* If you're using the JavaScript API, just add [`new NodePackageImporter()`] to
the `importers` option.

* If you're using the Dart API, add [`NodePackageImporter()`] to the `importers`
option.

* If you're using the command line, pass [`--pkg-importer=nodejs`].

[`new NodePackageImporter()`]: /documentation/js-api/classes/NodePackageImporter/
[`NodePackageImporter()`]: https://pub.dev/documentation/sass/latest/sass/NodePackageImporter-class.html
[`--pkg-importer=nodejs`]: /documentation/cli/dart-sass/#pkg-importer-nodejs

If you load a `pkg:` URL, the Node.js `pkg:` importer will look at its
`package.json` file to determine which Sass file to load. It will check in
order:

* The [`"exports"` field], with the conditions `"sass"`, `"style"`, and
`"default"`. This is the recommended way for packages to expose Sass
entrypoints going forward.

* The `"sass"` field or the `"style"` field, which should be a path to a Sass
file. This only works if the `pkg:` URL doesn't have a subpath—`pkg:library`
will load the file listed in the `"sass"` field, but `pkg:library/button` will
load `button.scss` from the root of the package.

* The [index file] at the root of the package This also only works if the `pkg:`
URL doesn't have a subpath.

[`"exports"` field]: https://nodejs.org/api/packages.html#conditional-exports
[index file]: /documentation/at-rules/use/#index-files

The Node.js `pkg:` importer supports the full range of `"exports"` features, so
you can also specify different locations for different subpaths (note that the
key must include the file extension):

```json
{
"exports": {
".": {
"sass": "styles/index.scss",
},
"./button.scss": {
"sass": "styles/button.scss",
},
"./accordion.scss": {
"sass": "styles/accordion.scss",
}
}
}
```

...or even patterns:

```json
{
"exports": {
".": {
"sass": "styles/index.scss",
},
"./*.scss": {
"sass": "styles/*.scss",
},
}
}
```

## Loading CSS

In addition to loading `.sass` and `.scss` files, Sass can load plain old `.css`
Expand Down
16 changes: 16 additions & 0 deletions source/documentation/cli/dart-sass.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ Earlier load paths will take precedence over later ones.
$ sass --load-path=node_modules/bootstrap/dist/css style.scss style.css
```

#### `--pkg-importer=nodejs`

{% compatibility 'dart: "1.71.0"' %}{% endcompatibility %}

This option (abbreviated `-p nodejs`) adds the [Node.js `pkg:` importer] to the
end of the load path, so that stylesheets can load dependencies using the
Node.js module resolution algorithm.

[Node.js `pkg:` importer]: /documentation/at-rules/use#node-js-package-importer

Support for additional built-in `pkg:` importers may be added in the future.

```shellsession
$ sass --pkg-importer=nodejs style.scss style.css
```

#### `--style`

This option (abbreviated `-s`) controls the output style of the resulting CSS.
Expand Down