Skip to content

Commit

Permalink
[Previous URL] Add a proposal
Browse files Browse the repository at this point in the history
See #3247
  • Loading branch information
nex3 committed Apr 20, 2023
1 parent a9ca204 commit 0821328
Showing 1 changed file with 185 additions and 0 deletions.
185 changes: 185 additions & 0 deletions proposal/prev-url.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* # Containing URL: Draft 1.0
*
* *([Issue](https://github.com/sass/sass/issues/3247))*
*
* ## Background
*
* > This section is non-normative.
*
* Among many other changes, the [new importer API] dropped an importer's
* ability to access the URL of the stylesheet that contained the load, known in
* the legacy API as the "previous URL". This was an intentional design choice
* which enforced the invariant that the same canonical URL always refers to the
* same file.
*
* [new import API]: ../accepted/new-js-importer.d.ts
*
* However, this restriction makes it difficult for importers to work as
* expected in certain contexts. For example, in the Node.js ecosystem JS loads
* depend on the structure of the `node_modules` directory closest to the
* containing file. The new import API can't match this behavior.
*
* This is particularly problematic for the widely-used Webpack importer, which
* expands on the concept of directory-specific load contexts to allow users to
* do fine-grained customization of how differnt files will load their
* dependencies. In order to ease migration to the new API for this plugin and
* its users, and to better match external ecosystems' load semantics, a
* solution is needed.
*
* ## Summary
*
* > This section is non-normative.
*
* This proposal adds an additional option to the `Importer.canonicalize()` API
* that provides the canonical URL of the containing file (the "containing
* URL"). However, in order to preserve the desired invariants, this option is
* only provided when either:
*
* - `Importer.canonicalize()` is being passed a relative URL (which means the
* URL has already been tried as a load relative to the current canonical
* URL), or
*
* - `Importer.canonicalize()` is passed an absolute URL whose scheme the
* importer has declared as non-canonical.
*
* A "non-canonical" scheme is a new concept introduced by this proposals.
* Importers will optionally be able to provide a `nonCanonicalScheme` field
* which will declare one or more URL schemes that they'll never return from
* `canonicalize()`. (If they do, Sass will throw an error.)
*
* ### Design Decisions
*
* #### Invariants
*
* The specific restrictions for this API were put in place to preserve the
* following invariants:
*
* 1. There must be a one-to-one mapping between canonical URLs and stylesheets.
* This means that even when a user loads a stylesheet using a relative URL,
* that stylesheet must have an absolute canonical URL associated with it
* *and* loading that canonical URL must return the same stylesheet. This
* means that any stylesheet can *always* be unambiguously loaded using its
* canonical URL.
*
* 2. Relative URLs are resolved like paths and HTTP URLs. For example, within
* `scheme:a/b/c.scss`, the URL `../d` should be resolved to `scheme:a/d`.
*
* 3. Loads relative to the current stylesheet always take precedence over loads
* from importers, so if `scheme:a/b/x.scss` exists then `@use "x"` within
* `scheme:a/b/c.scss` will always load it.
*
* #### Risks
*
* Providing access to the containing URL puts these invariants at risk in two ways:
*
* 1. Access to the containing URL in addition to a canonical URL makes it
* possible for importer authors to handle the same canonical URL differently
* depending in different contexts, violating invariant (1).
*
* 2. It's likely that importer authors familiar with the legacy API will
* incorrectly assume that any containing URL that exists is the best way to
* handle relative loads, since the only way to do so in the legacy API was
* to manually resolve them relative to the `prev` parameter. Doing so will
* almost certainly lead to violations of invariant (3) and possibly (2).
*
* #### Alternatives Considered
*
* To mitigate these risks, we need to have _some_ restriction on when the
* containing URL is available to importers. We considered the following
* alternative restrictions before settling on the current one:
*
* ##### Unavailable for Pre-Resolved Loads
*
* **Don't provide the containing URL when the `canonicalize()` function is
* called for pre-resolved relative loads.** When the user loads a relative URL,
* the Sass compiler first resolves that URL against the current canonical URL
* and passes the resulting absolute URL to the current importer's
* `canonicalize()` function. This invocation would not have access to the
* containing URL; all other invocations would, including when Sass passes the
* relative URL as-is to `canonicalize()`.
*
* This mitigates risk (2) by ensuring that all relative URL resolution is
* handled by the compiler by default. The importer will be invoked with an
* absolute URL and no containing URL first for each relative load, which will
* break for any importers that naïvely try to use the containing URL in all
* cases.
*
* This has several drawbacks. First, a badly-behaved importer could work around
* this by returning `null` for all relative loads and then manually resolving
* relative URLs as part of its load path resolution, thus continuing to violate
* invariant (3). Second, this provides no protection against risk (1) since the
* stylesheet author may still directly load a canonical URL.
*
* ##### Unavailable for Absolute Loads
*
* **Don't provide the containing URL when the `canonicalize()` function is
* being called for any absolute URL.** Since relative loads always pass
* absolute URLs to their importers first, this is a superset of "Unavailable
* for Pre-Resolved Loads". In addition, it protects against risk (1) by
* ensuring that all absolute URLs (which are a superset of canonical URLs) are
* canonicalized without regard to context.
*
* However, this limits the functionality of importers that use a custom URL
* scheme for *non-canonical* URLs. For example, if we choose to support
* [package imports] by claiming the `pkg:` scheme as a "built-in package
* importer", implementations of this scheme wouldn't be able to do
* context-sensitive resolution. This would make the scheme useless for
* supporting Node-style resolution, a core use-case. Given that we want to
* encourage users to use URL schemes rather than relative URLs, this is a
* blocking limitation.
*
* [package imports]: https://github.com/sass/sass/issues/2739
*
* Thus we arrive at the actual behavior, which makes the containing URL
* unavailable for absolute loads _unless_ they have a URL scheme declared
* explicitly non-canonical. This supports the `pkg:` use-case while still
* protecting against risk (1), since the containing URL is never available for
* canonical resolutions.
*/

import {PromiseOr} from '../spec/js-api/util/promise_or';

declare module '../spec/js-api/importer' {
/**
* Replace the first two bullet points for [invoking an importer with a
* string] with:
*
* [invoking an importer with a string]: ../spec/js-api/importer.d.ts
*
* - Let `fromImport` be `true` if the importer is being run for an `@import`
* and `false` otherwise.
*
* - If `string` is a relative URL, or if it's an absolute URL whose scheme is
* non-canonical for this importer, let `containingUrl` be the canonical URL
* of the [current source file]. Otherwise, or if the current source file
* has no canonical URL, let `containingUrl` be undefined.
*
* [current source file]: ../spec/spec.md#current-source-file
*
* - Let `url` be the result of calling `canonicalize` with `string`,
* `fromImport`, and `containingUrl`. If it returns a promise, wait for it
* to complete and use its value instead, or rethrow its error if it
* rejects.
*/
interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
/**
* The set of URL schemes that are considered *non-canonical* for this
* importer. If this is a single string, treat it as a list containing that
* string.
*
* Before beginning compilation, throw an error if any element of this
* contains a character other than a lowercase ASCII letter, an ASCII
* numeral, U+002B (`+`), U+002D (`-`), or U+002E (`.`).
*
* > Uppercase letters are normalized to lowercase in the `URL` constructor,
* > so for simplicity and efficiency we only allow lowercase here.
*/
nonCanonicalScheme?: string | string[];

canonicalize(
url: string,
options: {fromImport: boolean; containingUrl?: URL}
): PromiseOr<URL | null, sync>;
}
}

0 comments on commit 0821328

Please sign in to comment.