Skip to content

Provide access to the containing stylesheet's URL for some loads #3247

@nex3

Description

@nex3

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:

  1. 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.

  2. Relative URLs are resolved like paths, so for example within scheme:a/b/c.scss the URL ../d should always be resolved to scheme:a/d.

  3. 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.scss exists then @use "x" within scheme:a/b/c.scss will always load.

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).

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:

  1. 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 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.

  2. 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.

  3. 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: string field which indicates a URL scheme that the importer supports for canonicalize() but that it will never return from canonicalize(). 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    JS APIAbout the shared JS APIenhancementNew feature or requestproposal acceptedA full proposal for this feature has been marked accepted

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions