Skip to content

Commit 9caa0f3

Browse files
authored
Merge pull request #1309 from sass/fromImport
Add the ability for importers to detect @import
2 parents 5d4950d + 818d0d1 commit 9caa0f3

File tree

12 files changed

+186
-61
lines changed

12 files changed

+186
-61
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
## 1.33.0
2+
3+
### JS API
4+
5+
* The `this` context for importers now has a `fromImport` field, which is `true`
6+
if the importer is being invoked from an `@import` and `false` otherwise.
7+
Importers should only use this to determine whether to load [import-only
8+
files].
9+
10+
[import-only files]: https://sass-lang.com/documentation/at-rules/import#import-only-files
11+
12+
### Dart API
13+
14+
* Add an `Importer.fromImport` getter, which is `true` if the current
15+
`Importer.canonicalize()` call comes from an `@import` rule and `false`
16+
otherwise. Importers should only use this to determine whether to load
17+
[import-only files].
18+
119
## 1.32.13
220

321
* **Potentially breaking bug fix:** Null values in `@use` and `@forward`

lib/src/importer/async.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
import 'dart:async';
66

7+
import 'package:meta/meta.dart';
8+
79
import 'result.dart';
10+
import 'utils.dart' as utils;
811

912
/// An interface for importers that resolves URLs in `@import`s to the contents
1013
/// of Sass files.
@@ -20,6 +23,22 @@ import 'result.dart';
2023
///
2124
/// Subclasses should extend [AsyncImporter], not implement it.
2225
abstract class AsyncImporter {
26+
/// Whether the current [canonicalize] invocation comes from an `@import`
27+
/// rule.
28+
///
29+
/// When evaluating `@import` rules, URLs should canonicalize to an
30+
/// [import-only file] if one exists for the URL being canonicalized.
31+
/// Otherwise, canonicalization should be identical for `@import` and `@use`
32+
/// rules.
33+
///
34+
/// [import-only file]: https://sass-lang.com/documentation/at-rules/import#import-only-files
35+
///
36+
/// Subclasses should only access this from within calls to [canonicalize].
37+
/// Outside of that context, its value is undefined and subject to change.
38+
@protected
39+
@nonVirtual
40+
bool get fromImport => utils.fromImport;
41+
2342
/// If [url] is recognized by this importer, returns its canonical format.
2443
///
2544
/// Note that canonical URLs *must* be absolute, including a scheme. Returning

lib/src/importer/node/implementation.dart

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../../node/function.dart';
1313
import '../../node/importer_result.dart';
1414
import '../../node/utils.dart';
1515
import '../../util/nullable.dart';
16+
import '../../node/render_context.dart';
1617
import '../utils.dart';
1718

1819
/// An importer that encapsulates Node Sass's import logic.
@@ -40,8 +41,12 @@ import '../utils.dart';
4041
/// 4. Filesystem imports relative to an `includePaths` path.
4142
/// 5. Filesystem imports relative to a `SASS_PATH` path.
4243
class NodeImporter {
43-
/// The `this` context in which importer functions are invoked.
44-
final Object _context;
44+
/// The options for the `this` context in which importer functions are
45+
/// invoked.
46+
///
47+
/// This is typed as [Object] because the public interface of [NodeImporter]
48+
/// is shared with the VM, which can't handle JS interop types.
49+
final Object _options;
4550

4651
/// The include paths passed in by the user.
4752
final List<String> _includePaths;
@@ -50,7 +55,7 @@ class NodeImporter {
5055
final List<JSFunction> _importers;
5156

5257
NodeImporter(
53-
this._context, Iterable<String> includePaths, Iterable<Object> importers)
58+
this._options, Iterable<String> includePaths, Iterable<Object> importers)
5459
: _includePaths = List.unmodifiable(_addSassPath(includePaths)),
5560
_importers = List.unmodifiable(importers.cast());
5661

@@ -78,7 +83,8 @@ class NodeImporter {
7883
// The previous URL is always an absolute file path for filesystem imports.
7984
var previousString = _previousToString(previous);
8085
for (var importer in _importers) {
81-
var value = call2(importer, _context, url, previousString);
86+
var value =
87+
call2(importer, _renderContext(forImport), url, previousString);
8288
if (value != null) {
8389
return _handleImportResult(url, previous, value, forImport);
8490
}
@@ -103,7 +109,8 @@ class NodeImporter {
103109
// The previous URL is always an absolute file path for filesystem imports.
104110
var previousString = _previousToString(previous);
105111
for (var importer in _importers) {
106-
var value = await _callImporterAsync(importer, url, previousString);
112+
var value =
113+
await _callImporterAsync(importer, url, previousString, forImport);
107114
if (value != null) {
108115
return _handleImportResult(url, previous, value, forImport);
109116
}
@@ -193,13 +200,21 @@ class NodeImporter {
193200
}
194201

195202
/// Calls an importer that may or may not be asynchronous.
196-
Future<Object?> _callImporterAsync(
197-
JSFunction importer, String url, String previousString) async {
203+
Future<Object?> _callImporterAsync(JSFunction importer, String url,
204+
String previousString, bool forImport) async {
198205
var completer = Completer<Object>();
199206

200-
var result = call3(importer, _context, url, previousString,
207+
var result = call3(importer, _renderContext(forImport), url, previousString,
201208
allowInterop(completer.complete));
202209
if (isUndefined(result)) return await completer.future;
203210
return result;
204211
}
212+
213+
/// Returns the [RenderContext] in which to invoke importers.
214+
RenderContext _renderContext(bool fromImport) {
215+
var context = RenderContext(
216+
options: _options as RenderContextOptions, fromImport: fromImport);
217+
context.options.context = context;
218+
return context;
219+
}
205220
}

lib/src/importer/node/interface.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import 'package:tuple/tuple.dart';
66

77
class NodeImporter {
8-
NodeImporter(Object context, Iterable<String> includePaths,
8+
NodeImporter(Object options, Iterable<String> includePaths,
99
Iterable<Object> importers);
1010

1111
Tuple2<String, String>? load(String url, Uri? previous, bool forImport) =>

lib/src/importer/utils.dart

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

5+
import 'dart:async';
6+
57
import 'package:path/path.dart' as p;
68

79
import '../io.dart';
@@ -13,30 +15,12 @@ import '../io.dart';
1315
/// canonicalization should be identical for `@import` and `@use` rules. It's
1416
/// admittedly hacky to set this globally, but `@import` will eventually be
1517
/// removed, at which point we can delete this and have one consistent behavior.
16-
bool _inImportRule = false;
17-
18-
/// Runs [callback] in a context where [resolveImportPath] uses `@import`
19-
/// semantics rather than `@use` semantics.
20-
T inImportRule<T>(T callback()) {
21-
var wasInImportRule = _inImportRule;
22-
_inImportRule = true;
23-
try {
24-
return callback();
25-
} finally {
26-
_inImportRule = wasInImportRule;
27-
}
28-
}
18+
bool get fromImport => Zone.current[#_inImportRule] as bool? ?? false;
2919

30-
/// Like [inImportRule], but asynchronous.
31-
Future<T> inImportRuleAsync<T>(Future<T> callback()) async {
32-
var wasInImportRule = _inImportRule;
33-
_inImportRule = true;
34-
try {
35-
return await callback();
36-
} finally {
37-
_inImportRule = wasInImportRule;
38-
}
39-
}
20+
/// Runs [callback] in a context where [inImportRule] returns `true` and
21+
/// [resolveImportPath] uses `@import` semantics rather than `@use` semantics.
22+
T inImportRule<T>(T callback()) =>
23+
runZoned(callback, zoneValues: {#_inImportRule: true});
4024

4125
/// Resolves an imported path using the same logic as the filesystem importer.
4226
///
@@ -95,7 +79,7 @@ String? _exactlyOne(List<String> paths) {
9579
paths.map((path) => " " + p.prettyUri(p.toUri(path))).join("\n");
9680
}
9781

98-
/// If [_inImportRule] is `true`, invokes callback and returns the result.
82+
/// If [fromImport] is `true`, invokes callback and returns the result.
9983
///
10084
/// Otherwise, returns `null`.
101-
T? _ifInImport<T>(T callback()) => _inImportRule ? callback() : null;
85+
T? _ifInImport<T>(T callback()) => fromImport ? callback() : null;

lib/src/node.dart

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ List<AsyncCallable> _parseFunctions(RenderOptions options, DateTime start,
204204
'Invalid signature "$signature": ${error.message}', error.span);
205205
}
206206

207-
var context = _contextWithOptions(options, start);
207+
var context = RenderContext(options: _contextOptions(options, start));
208+
context.options.context = context;
208209

209210
var fiber = options.fiber;
210211
if (fiber != null) {
@@ -261,9 +262,8 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
261262
importers = [options.importer as JSFunction];
262263
}
263264

264-
var context = importers.isNotEmpty
265-
? _contextWithOptions(options, start)
266-
: const Object();
265+
var contextOptions =
266+
importers.isNotEmpty ? _contextOptions(options, start) : Object();
267267

268268
var fiber = options.fiber;
269269
if (fiber != null) {
@@ -288,29 +288,26 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
288288
}
289289

290290
var includePaths = List<String>.from(options.includePaths ?? []);
291-
return NodeImporter(context, includePaths, importers);
291+
return NodeImporter(contextOptions, includePaths, importers);
292292
}
293293

294-
/// Creates a `this` context that contains the render options.
295-
RenderContext _contextWithOptions(RenderOptions options, DateTime start) {
294+
/// Creates the [RenderContextOptions] for the `this` context in which custom
295+
/// functions and importers will be evaluated.
296+
RenderContextOptions _contextOptions(RenderOptions options, DateTime start) {
296297
var includePaths = List<String>.from(options.includePaths ?? []);
297-
var context = RenderContext(
298-
options: RenderContextOptions(
299-
file: options.file,
300-
data: options.data,
301-
includePaths:
302-
([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
303-
precision: SassNumber.precision,
304-
style: 1,
305-
indentType: options.indentType == 'tab' ? 1 : 0,
306-
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
307-
linefeed: _parseLineFeed(options.linefeed).text,
308-
result: RenderContextResult(
309-
stats: RenderContextResultStats(
310-
start: start.millisecondsSinceEpoch,
311-
entry: options.file ?? 'data'))));
312-
context.options.context = context;
313-
return context;
298+
return RenderContextOptions(
299+
file: options.file,
300+
data: options.data,
301+
includePaths: ([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
302+
precision: SassNumber.precision,
303+
style: 1,
304+
indentType: options.indentType == 'tab' ? 1 : 0,
305+
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
306+
linefeed: _parseLineFeed(options.linefeed).text,
307+
result: RenderContextResult(
308+
stats: RenderContextResultStats(
309+
start: start.millisecondsSinceEpoch,
310+
entry: options.file ?? 'data')));
314311
}
315312

316313
/// Parse [style] into an [OutputStyle].

lib/src/node/render_context.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import 'package:js/js.dart';
88
@anonymous
99
class RenderContext {
1010
external RenderContextOptions get options;
11+
external bool? get fromImport;
1112

12-
external factory RenderContext({required RenderContextOptions options});
13+
external factory RenderContext(
14+
{required RenderContextOptions options, bool? fromImport});
1315
}
1416

1517
@JS()

lib/src/parse/sass.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ class SassParser extends StylesheetParser {
414414
do {
415415
containsTab = false;
416416
containsSpace = false;
417-
nextIndentation = 0;
417+
nextIndentation = 0;
418418

419419
while (true) {
420420
var next = scanner.peekChar();

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.32.13
2+
version: 1.33.0-dev
33
description: A Sass implementation in Dart.
44
author: Sass Team
55
homepage: https://github.com/sass/dart-sass
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2017 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:sass/sass.dart';
6+
import 'package:test/test.dart';
7+
8+
/// An [Importer] whose [canonicalize] method asserts the value of
9+
/// [Importer.fromImport].
10+
class FromImportImporter extends Importer {
11+
/// The expected value of [Importer.fromImport] in the call to [canonicalize].
12+
final bool _expected;
13+
14+
/// The callback to call once [canonicalize] is called.
15+
///
16+
/// This ensures that the test doesn't exit until [canonicalize] is called.
17+
final void Function() _done;
18+
19+
FromImportImporter(this._expected) : _done = expectAsync0(() {});
20+
21+
Uri? canonicalize(Uri url) {
22+
expect(fromImport, equals(_expected));
23+
_done();
24+
return Uri.parse('u:');
25+
}
26+
27+
ImporterResult? load(Uri url) => ImporterResult("", syntax: Syntax.scss);
28+
}

test/dart_api/importer_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:test/test.dart';
1212
import 'package:sass/sass.dart';
1313
import 'package:sass/src/exception.dart';
1414

15+
import 'from_import_importer.dart';
1516
import 'test_importer.dart';
1617
import '../utils.dart';
1718

@@ -182,4 +183,23 @@ void main() {
182183
return true;
183184
})));
184185
});
186+
187+
group("currentLoadFromImport is", () {
188+
test("true from an @import", () {
189+
compileString('@import "foo"', importers: [FromImportImporter(true)]);
190+
});
191+
192+
test("false from a @use", () {
193+
compileString('@use "foo"', importers: [FromImportImporter(false)]);
194+
});
195+
196+
test("false from a @forward", () {
197+
compileString('@forward "foo"', importers: [FromImportImporter(false)]);
198+
});
199+
200+
test("false from meta.load-css", () {
201+
compileString('@use "sass:meta"; @include meta.load-css("")',
202+
importers: [FromImportImporter(false)]);
203+
});
204+
});
185205
}

0 commit comments

Comments
 (0)