Skip to content

Commit 13c9fb3

Browse files
authored
Expose the containing URL to importers under some circumstances (#2083)
Closes #1946
1 parent 69f1847 commit 13c9fb3

File tree

22 files changed

+574
-98
lines changed

22 files changed

+574
-98
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ jobs:
137137
sass_spec_js_embedded:
138138
name: 'JS API Tests | Embedded | Node ${{ matrix.node-version }} | ${{ matrix.os }}'
139139
runs-on: ${{ matrix.os }}-latest
140+
if: "github.event_name != 'pull_request' || !contains(github.event.pull_request.body, 'skip sass-embedded')"
140141

141142
strategy:
142143
fail-fast: false

lib/src/async_import_cache.dart

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ final class AsyncImportCache {
122122

123123
/// Canonicalizes [url] according to one of this cache's importers.
124124
///
125+
/// The [baseUrl] should be the canonical URL of the stylesheet that contains
126+
/// the load, if it exists.
127+
///
125128
/// Returns the importer that was used to canonicalize [url], the canonical
126129
/// URL, and the URL that was passed to the importer (which may be resolved
127130
/// relative to [baseUrl] if it's passed).
@@ -139,33 +142,30 @@ final class AsyncImportCache {
139142
if (isBrowser &&
140143
(baseImporter == null || baseImporter is NoOpImporter) &&
141144
_importers.isEmpty) {
142-
throw "Custom importers are required to load stylesheets when compiling in the browser.";
145+
throw "Custom importers are required to load stylesheets when compiling "
146+
"in the browser.";
143147
}
144148

145149
if (baseImporter != null && url.scheme == '') {
146-
var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, (
147-
url,
148-
forImport: forImport,
149-
baseImporter: baseImporter,
150-
baseUrl: baseUrl
151-
), () async {
152-
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
153-
if (await _canonicalize(baseImporter, resolvedUrl, forImport)
154-
case var canonicalUrl?) {
155-
return (baseImporter, canonicalUrl, originalUrl: resolvedUrl);
156-
} else {
157-
return null;
158-
}
159-
});
150+
var relativeResult = await putIfAbsentAsync(
151+
_relativeCanonicalizeCache,
152+
(
153+
url,
154+
forImport: forImport,
155+
baseImporter: baseImporter,
156+
baseUrl: baseUrl
157+
),
158+
() => _canonicalize(baseImporter, baseUrl?.resolveUri(url) ?? url,
159+
baseUrl, forImport));
160160
if (relativeResult != null) return relativeResult;
161161
}
162162

163163
return await putIfAbsentAsync(
164164
_canonicalizeCache, (url, forImport: forImport), () async {
165165
for (var importer in _importers) {
166-
if (await _canonicalize(importer, url, forImport)
167-
case var canonicalUrl?) {
168-
return (importer, canonicalUrl, originalUrl: url);
166+
if (await _canonicalize(importer, url, baseUrl, forImport)
167+
case var result?) {
168+
return result;
169169
}
170170
}
171171

@@ -175,18 +175,36 @@ final class AsyncImportCache {
175175

176176
/// Calls [importer.canonicalize] and prints a deprecation warning if it
177177
/// returns a relative URL.
178-
Future<Uri?> _canonicalize(
179-
AsyncImporter importer, Uri url, bool forImport) async {
180-
var result = await (forImport
181-
? inImportRule(() => importer.canonicalize(url))
182-
: importer.canonicalize(url));
183-
if (result?.scheme == '') {
184-
_logger.warnForDeprecation(Deprecation.relativeCanonical, """
185-
Importer $importer canonicalized $url to $result.
186-
Relative canonical URLs are deprecated and will eventually be disallowed.
187-
""");
178+
///
179+
/// If [resolveUrl] is `true`, this resolves [url] relative to [baseUrl]
180+
/// before passing it to [importer].
181+
Future<AsyncCanonicalizeResult?> _canonicalize(
182+
AsyncImporter importer, Uri url, Uri? baseUrl, bool forImport,
183+
{bool resolveUrl = false}) async {
184+
var resolved =
185+
resolveUrl && baseUrl != null ? baseUrl.resolveUri(url) : url;
186+
var canonicalize = forImport
187+
? () => inImportRule(() => importer.canonicalize(resolved))
188+
: () => importer.canonicalize(resolved);
189+
190+
var passContainingUrl = baseUrl != null &&
191+
(url.scheme == '' || await importer.isNonCanonicalScheme(url.scheme));
192+
var result = await withContainingUrl(
193+
passContainingUrl ? baseUrl : null, canonicalize);
194+
if (result == null) return null;
195+
196+
if (result.scheme == '') {
197+
_logger.warnForDeprecation(
198+
Deprecation.relativeCanonical,
199+
"Importer $importer canonicalized $resolved to $result.\n"
200+
"Relative canonical URLs are deprecated and will eventually be "
201+
"disallowed.");
202+
} else if (await importer.isNonCanonicalScheme(result.scheme)) {
203+
throw "Importer $importer canonicalized $resolved to $result, which "
204+
"uses a scheme declared as non-canonical.";
188205
}
189-
return result;
206+
207+
return (importer, result, originalUrl: resolved);
190208
}
191209

192210
/// Tries to import [url] using one of this cache's importers.

lib/src/embedded/dispatcher.dart

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,32 @@ final class Dispatcher {
206206
InboundMessage_CompileRequest_Importer importer) {
207207
switch (importer.whichImporter()) {
208208
case InboundMessage_CompileRequest_Importer_Importer.path:
209+
_checkNoNonCanonicalScheme(importer);
209210
return sass.FilesystemImporter(importer.path);
210211

211212
case InboundMessage_CompileRequest_Importer_Importer.importerId:
212-
return HostImporter(this, importer.importerId);
213+
return HostImporter(
214+
this, importer.importerId, importer.nonCanonicalScheme);
213215

214216
case InboundMessage_CompileRequest_Importer_Importer.fileImporterId:
217+
_checkNoNonCanonicalScheme(importer);
215218
return FileImporter(this, importer.fileImporterId);
216219

217220
case InboundMessage_CompileRequest_Importer_Importer.notSet:
221+
_checkNoNonCanonicalScheme(importer);
218222
return null;
219223
}
220224
}
221225

226+
/// Throws a [ProtocolError] if [importer] contains one or more
227+
/// `nonCanonicalScheme`s.
228+
void _checkNoNonCanonicalScheme(
229+
InboundMessage_CompileRequest_Importer importer) {
230+
if (importer.nonCanonicalScheme.isEmpty) return;
231+
throw paramsError("Importer.non_canonical_scheme may only be set along "
232+
"with Importer.importer.importer_id");
233+
}
234+
222235
/// Sends [event] to the host.
223236
void sendLog(OutboundMessage_LogEvent event) =>
224237
_send(OutboundMessage()..logEvent = event);
@@ -278,9 +291,7 @@ final class Dispatcher {
278291
InboundMessage_Message.versionRequest =>
279292
throw paramsError("VersionRequest must have compilation ID 0."),
280293
InboundMessage_Message.notSet =>
281-
throw parseError("InboundMessage.message is not set."),
282-
_ =>
283-
throw parseError("Unknown message type: ${message.toDebugString()}")
294+
throw parseError("InboundMessage.message is not set.")
284295
};
285296

286297
if (message.id != _outboundRequestId) {

lib/src/embedded/importer/file.dart

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ final class FileImporter extends ImporterBase {
2424
Uri? canonicalize(Uri url) {
2525
if (url.scheme == 'file') return _filesystemImporter.canonicalize(url);
2626

27-
var response =
28-
dispatcher.sendFileImportRequest(OutboundMessage_FileImportRequest()
29-
..importerId = _importerId
30-
..url = url.toString()
31-
..fromImport = fromImport);
27+
var request = OutboundMessage_FileImportRequest()
28+
..importerId = _importerId
29+
..url = url.toString()
30+
..fromImport = fromImport;
31+
if (containingUrl case var containingUrl?) {
32+
request.containingUrl = containingUrl.toString();
33+
}
34+
var response = dispatcher.sendFileImportRequest(request);
3235

3336
switch (response.whichResult()) {
3437
case InboundMessage_FileImportResponse_Result.fileUrl:
@@ -49,5 +52,7 @@ final class FileImporter extends ImporterBase {
4952

5053
ImporterResult? load(Uri url) => _filesystemImporter.load(url);
5154

55+
bool isNonCanonicalScheme(String scheme) => scheme != 'file';
56+
5257
String toString() => "FileImporter";
5358
}

lib/src/embedded/importer/host.dart

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import '../../exception.dart';
56
import '../../importer.dart';
7+
import '../../importer/utils.dart';
8+
import '../../util/span.dart';
69
import '../dispatcher.dart';
710
import '../embedded_sass.pb.dart' hide SourceSpan;
811
import '../utils.dart';
@@ -13,14 +16,31 @@ final class HostImporter extends ImporterBase {
1316
/// The host-provided ID of the importer to invoke.
1417
final int _importerId;
1518

16-
HostImporter(Dispatcher dispatcher, this._importerId) : super(dispatcher);
19+
/// The set of URL schemes that this importer promises never to return from
20+
/// [canonicalize].
21+
final Set<String> _nonCanonicalSchemes;
22+
23+
HostImporter(Dispatcher dispatcher, this._importerId,
24+
Iterable<String> nonCanonicalSchemes)
25+
: _nonCanonicalSchemes = Set.unmodifiable(nonCanonicalSchemes),
26+
super(dispatcher) {
27+
for (var scheme in _nonCanonicalSchemes) {
28+
if (isValidUrlScheme(scheme)) continue;
29+
throw SassException(
30+
'"$scheme" isn\'t a valid URL scheme (for example "file").',
31+
bogusSpan);
32+
}
33+
}
1734

1835
Uri? canonicalize(Uri url) {
19-
var response =
20-
dispatcher.sendCanonicalizeRequest(OutboundMessage_CanonicalizeRequest()
21-
..importerId = _importerId
22-
..url = url.toString()
23-
..fromImport = fromImport);
36+
var request = OutboundMessage_CanonicalizeRequest()
37+
..importerId = _importerId
38+
..url = url.toString()
39+
..fromImport = fromImport;
40+
if (containingUrl case var containingUrl?) {
41+
request.containingUrl = containingUrl.toString();
42+
}
43+
var response = dispatcher.sendCanonicalizeRequest(request);
2444

2545
return switch (response.whichResult()) {
2646
InboundMessage_CanonicalizeResponse_Result.url =>
@@ -47,5 +67,8 @@ final class HostImporter extends ImporterBase {
4767
};
4868
}
4969

70+
bool isNonCanonicalScheme(String scheme) =>
71+
_nonCanonicalSchemes.contains(scheme);
72+
5073
String toString() => "HostImporter";
5174
}

lib/src/import_cache.dart

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// DO NOT EDIT. This file was generated from async_import_cache.dart.
66
// See tool/grind/synchronize.dart for details.
77
//
8-
// Checksum: 3e4cae79c03ce2af6626b1822f1468523b401e90
8+
// Checksum: ff52307a3bc93358ddc46f1e76120894fa3e071f
99
//
1010
// ignore_for_file: unused_import
1111

@@ -124,6 +124,9 @@ final class ImportCache {
124124

125125
/// Canonicalizes [url] according to one of this cache's importers.
126126
///
127+
/// The [baseUrl] should be the canonical URL of the stylesheet that contains
128+
/// the load, if it exists.
129+
///
127130
/// Returns the importer that was used to canonicalize [url], the canonical
128131
/// URL, and the URL that was passed to the importer (which may be resolved
129132
/// relative to [baseUrl] if it's passed).
@@ -139,31 +142,27 @@ final class ImportCache {
139142
if (isBrowser &&
140143
(baseImporter == null || baseImporter is NoOpImporter) &&
141144
_importers.isEmpty) {
142-
throw "Custom importers are required to load stylesheets when compiling in the browser.";
145+
throw "Custom importers are required to load stylesheets when compiling "
146+
"in the browser.";
143147
}
144148

145149
if (baseImporter != null && url.scheme == '') {
146-
var relativeResult = _relativeCanonicalizeCache.putIfAbsent((
147-
url,
148-
forImport: forImport,
149-
baseImporter: baseImporter,
150-
baseUrl: baseUrl
151-
), () {
152-
var resolvedUrl = baseUrl?.resolveUri(url) ?? url;
153-
if (_canonicalize(baseImporter, resolvedUrl, forImport)
154-
case var canonicalUrl?) {
155-
return (baseImporter, canonicalUrl, originalUrl: resolvedUrl);
156-
} else {
157-
return null;
158-
}
159-
});
150+
var relativeResult = _relativeCanonicalizeCache.putIfAbsent(
151+
(
152+
url,
153+
forImport: forImport,
154+
baseImporter: baseImporter,
155+
baseUrl: baseUrl
156+
),
157+
() => _canonicalize(baseImporter, baseUrl?.resolveUri(url) ?? url,
158+
baseUrl, forImport));
160159
if (relativeResult != null) return relativeResult;
161160
}
162161

163162
return _canonicalizeCache.putIfAbsent((url, forImport: forImport), () {
164163
for (var importer in _importers) {
165-
if (_canonicalize(importer, url, forImport) case var canonicalUrl?) {
166-
return (importer, canonicalUrl, originalUrl: url);
164+
if (_canonicalize(importer, url, baseUrl, forImport) case var result?) {
165+
return result;
167166
}
168167
}
169168

@@ -173,17 +172,36 @@ final class ImportCache {
173172

174173
/// Calls [importer.canonicalize] and prints a deprecation warning if it
175174
/// returns a relative URL.
176-
Uri? _canonicalize(Importer importer, Uri url, bool forImport) {
177-
var result = (forImport
178-
? inImportRule(() => importer.canonicalize(url))
179-
: importer.canonicalize(url));
180-
if (result?.scheme == '') {
181-
_logger.warnForDeprecation(Deprecation.relativeCanonical, """
182-
Importer $importer canonicalized $url to $result.
183-
Relative canonical URLs are deprecated and will eventually be disallowed.
184-
""");
175+
///
176+
/// If [resolveUrl] is `true`, this resolves [url] relative to [baseUrl]
177+
/// before passing it to [importer].
178+
CanonicalizeResult? _canonicalize(
179+
Importer importer, Uri url, Uri? baseUrl, bool forImport,
180+
{bool resolveUrl = false}) {
181+
var resolved =
182+
resolveUrl && baseUrl != null ? baseUrl.resolveUri(url) : url;
183+
var canonicalize = forImport
184+
? () => inImportRule(() => importer.canonicalize(resolved))
185+
: () => importer.canonicalize(resolved);
186+
187+
var passContainingUrl = baseUrl != null &&
188+
(url.scheme == '' || importer.isNonCanonicalScheme(url.scheme));
189+
var result =
190+
withContainingUrl(passContainingUrl ? baseUrl : null, canonicalize);
191+
if (result == null) return null;
192+
193+
if (result.scheme == '') {
194+
_logger.warnForDeprecation(
195+
Deprecation.relativeCanonical,
196+
"Importer $importer canonicalized $resolved to $result.\n"
197+
"Relative canonical URLs are deprecated and will eventually be "
198+
"disallowed.");
199+
} else if (importer.isNonCanonicalScheme(result.scheme)) {
200+
throw "Importer $importer canonicalized $resolved to $result, which "
201+
"uses a scheme declared as non-canonical.";
185202
}
186-
return result;
203+
204+
return (importer, result, originalUrl: resolved);
187205
}
188206

189207
/// Tries to import [url] using one of this cache's importers.

lib/src/importer.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,6 @@ abstract class Importer extends AsyncImporter {
4040
DateTime modificationTime(Uri url) => DateTime.now();
4141

4242
bool couldCanonicalize(Uri url, Uri canonicalUrl) => true;
43+
44+
bool isNonCanonicalScheme(String scheme) => false;
4345
}

0 commit comments

Comments
 (0)