-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
- Proposal ([Previous URL] Add a proposal #3550)
- Dart Sass (Expose the containing URL to importers under some circumstances dart-sass#2083)
- Embedded Host (Pass the containing URL to importers under some circumstances embedded-host-node#246)
- Tests (Add tests for containing URL support sass-spec#1936)
- Update specification ([Containing URL] Update the specification #3688)
In webpack/sass-loader#774, @alexander-akait requested some way to access the canonical URL of the stylesheet that contains a @use or @import when canonicalizing a URL in a custom importer.
The motivating use-case is supporting Node-style name resolution where different node_modules directories are used for dependencies in different packages. For example, to match Node's dependency resolution logic, @import "bar" in node_modules/foo/style.scss should prefer node_modules/foo/node_modules/bar/_index.scss to node_modules/bar/_index.scss.
Invariants
We need to support this behavior without making it easy for importers to violate the invariants I described in this comment:
There must be a one-to-one mapping between (absolute) 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 loaded using its canonical URL.
Relative URLs are resolved like paths, so for example within
scheme:a/b/c.scssthe URL../dshould always be resolved toscheme:a/d.Loads relative to the current stylesheet always take precedence over loads from the load path (including virtual load paths exposed by importers), so if
scheme:a/b/x.scssexists then@use "x"withinscheme:a/b/c.scsswill always load.
Risks
Providing access to the containing URL puts these invariants at risk in two ways:
-
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).
-
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
prevparameter. Doing so will almost certainly lead to violations of invariant (3) and possibly (2).
Mitigations
We can mitigate these risks by making the containing URL available only under certain circumstances. Offhand, I see three possible restrictions we could put in place:
-
Don't provide the containing URL when the
canonicalize()function is being called for a relative load. 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
nullfor 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. -
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, this is a superset of mitigation (1). 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 doesn't do anything to address a poorly-behaved importer's ability to continue to violate invariant (3). Worse, it limits the functionality of importers that use a custom URL scheme for non-canonical URLs. For example, if we choose to support Ship a built-in package importer #2739 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. -
Allow importers to opt into receiving the containing URL by agreeing to additional restrictions. This is motivated by the observation that the primary drawback to mitigation (2) is that we effectively want the same importer to be able to support multiple different URL schemes, some that are canonical and some that are not. To allow for context-dependent canonicalization of non-canonical absolute URLs, we could for example allow an importer to define a
nonCanonicalScheme: stringfield which indicates a URL scheme that the importer supports forcanonicalize()but that it will never return fromcanonicalize(). Then we could provide the containing URL specifically for resolutions of URLs with that scheme, and throw an error if the importer returns a URL of the same scheme.However, this API would still involve passing the containing URL to all relative loads, so it still doesn't fully eliminate risk (2). It's worth considering if there's a different restriction we could impose that would eliminate both risk (1) and risk (2).