diff --git a/source/blog/040-announcing-pkg-importers.md b/source/blog/040-announcing-pkg-importers.md new file mode 100644 index 000000000..a9c6e5bbd --- /dev/null +++ b/source/blog/040-announcing-pkg-importers.md @@ -0,0 +1,96 @@ +--- +title: "Announcing `pkg:` Importers" +author: Natalie Weizenbaum +date: 2024-02-15 17:00:00 -8 +--- + +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", + }, + } +} +``` diff --git a/source/documentation/at-rules/use.md b/source/documentation/at-rules/use.md index 7d932c25d..cea2c3e7c 100644 --- a/source/documentation/at-rules/use.md +++ b/source/documentation/at-rules/use.md @@ -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 @@ -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` diff --git a/source/documentation/cli/dart-sass.md b/source/documentation/cli/dart-sass.md index a35dec030..29622f12f 100644 --- a/source/documentation/cli/dart-sass.md +++ b/source/documentation/cli/dart-sass.md @@ -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.