Skip to content

Commit

Permalink
Add this.fromImport for JS importers
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed May 14, 2021
1 parent 5d4950d commit 8a344e3
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 36 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,14 @@
## 1.33.0

### JS API

* The `this` context for importers now has a `fromImport` field, which is `true`
if the importer is being invoked from an `@import` and `false` otherwise.
Importers should only use this to determine whether to load [import-only
files].

[import-only files]: https://sass-lang.com/documentation/at-rules/import#import-only-files

## 1.32.13

* **Potentially breaking bug fix:** Null values in `@use` and `@forward`
Expand Down
27 changes: 19 additions & 8 deletions lib/src/importer/node/implementation.dart
Expand Up @@ -13,6 +13,7 @@ import '../../node/function.dart';
import '../../node/importer_result.dart';
import '../../node/utils.dart';
import '../../util/nullable.dart';
import '../../node/render_context.dart';
import '../utils.dart';

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

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

NodeImporter(
this._context, Iterable<String> includePaths, Iterable<Object> importers)
this._options, Iterable<String> includePaths, Iterable<Object> importers)
: _includePaths = List.unmodifiable(_addSassPath(includePaths)),
_importers = List.unmodifiable(importers.cast());

Expand Down Expand Up @@ -78,7 +83,8 @@ class NodeImporter {
// The previous URL is always an absolute file path for filesystem imports.
var previousString = _previousToString(previous);
for (var importer in _importers) {
var value = call2(importer, _context, url, previousString);
var value =
call2(importer, _renderContext(forImport), url, previousString);
if (value != null) {
return _handleImportResult(url, previous, value, forImport);
}
Expand All @@ -103,7 +109,8 @@ class NodeImporter {
// The previous URL is always an absolute file path for filesystem imports.
var previousString = _previousToString(previous);
for (var importer in _importers) {
var value = await _callImporterAsync(importer, url, previousString);
var value =
await _callImporterAsync(importer, url, previousString, forImport);
if (value != null) {
return _handleImportResult(url, previous, value, forImport);
}
Expand Down Expand Up @@ -193,13 +200,17 @@ class NodeImporter {
}

/// Calls an importer that may or may not be asynchronous.
Future<Object?> _callImporterAsync(
JSFunction importer, String url, String previousString) async {
Future<Object?> _callImporterAsync(JSFunction importer, String url,
String previousString, bool forImport) async {
var completer = Completer<Object>();

var result = call3(importer, _context, url, previousString,
var result = call3(importer, _renderContext(forImport), url, previousString,
allowInterop(completer.complete));
if (isUndefined(result)) return await completer.future;
return result;
}

/// Returns the [RenderContext] in which to invoke importers.
RenderContext _renderContext(bool fromImport) => RenderContext(
options: _options as RenderContextOptions, fromImport: fromImport);
}
2 changes: 1 addition & 1 deletion lib/src/importer/node/interface.dart
Expand Up @@ -5,7 +5,7 @@
import 'package:tuple/tuple.dart';

class NodeImporter {
NodeImporter(Object context, Iterable<String> includePaths,
NodeImporter(Object options, Iterable<String> includePaths,
Iterable<Object> importers);

Tuple2<String, String>? load(String url, Uri? previous, bool forImport) =>
Expand Down
44 changes: 20 additions & 24 deletions lib/src/node.dart
Expand Up @@ -204,7 +204,7 @@ List<AsyncCallable> _parseFunctions(RenderOptions options, DateTime start,
'Invalid signature "$signature": ${error.message}', error.span);
}

var context = _contextWithOptions(options, start);
var context = RenderContext(options: _contextOptions(options, start));

var fiber = options.fiber;
if (fiber != null) {
Expand Down Expand Up @@ -261,9 +261,8 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
importers = [options.importer as JSFunction];
}

var context = importers.isNotEmpty
? _contextWithOptions(options, start)
: const Object();
var contextOptions =
importers.isNotEmpty ? _contextOptions(options, start) : Object();

var fiber = options.fiber;
if (fiber != null) {
Expand All @@ -288,29 +287,26 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
}

var includePaths = List<String>.from(options.includePaths ?? []);
return NodeImporter(context, includePaths, importers);
return NodeImporter(contextOptions, includePaths, importers);
}

/// Creates a `this` context that contains the render options.
RenderContext _contextWithOptions(RenderOptions options, DateTime start) {
/// Creates the [RenderContextOptions] for the `this` context in which custom
/// functions and importers will be evaluated.
RenderContextOptions _contextOptions(RenderOptions options, DateTime start) {
var includePaths = List<String>.from(options.includePaths ?? []);
var context = RenderContext(
options: RenderContextOptions(
file: options.file,
data: options.data,
includePaths:
([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
precision: SassNumber.precision,
style: 1,
indentType: options.indentType == 'tab' ? 1 : 0,
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
linefeed: _parseLineFeed(options.linefeed).text,
result: RenderContextResult(
stats: RenderContextResultStats(
start: start.millisecondsSinceEpoch,
entry: options.file ?? 'data'))));
context.options.context = context;
return context;
return RenderContextOptions(
file: options.file,
data: options.data,
includePaths: ([p.current, ...includePaths]).join(isWindows ? ';' : ':'),
precision: SassNumber.precision,
style: 1,
indentType: options.indentType == 'tab' ? 1 : 0,
indentWidth: _parseIndentWidth(options.indentWidth) ?? 2,
linefeed: _parseLineFeed(options.linefeed).text,
result: RenderContextResult(
stats: RenderContextResultStats(
start: start.millisecondsSinceEpoch,
entry: options.file ?? 'data')));
}

/// Parse [style] into an [OutputStyle].
Expand Down
4 changes: 3 additions & 1 deletion lib/src/node/render_context.dart
Expand Up @@ -8,8 +8,10 @@ import 'package:js/js.dart';
@anonymous
class RenderContext {
external RenderContextOptions get options;
external bool? get fromImport;

external factory RenderContext({required RenderContextOptions options});
external factory RenderContext(
{required RenderContextOptions options, bool? fromImport});
}

@JS()
Expand Down
2 changes: 1 addition & 1 deletion lib/src/parse/sass.dart
Expand Up @@ -414,7 +414,7 @@ class SassParser extends StylesheetParser {
do {
containsTab = false;
containsSpace = false;
nextIndentation = 0;
nextIndentation = 0;

while (true) {
var next = scanner.peekChar();
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
@@ -1,5 +1,5 @@
name: sass
version: 1.32.13
version: 1.33.0-dev
description: A Sass implementation in Dart.
author: Sass Team
homepage: https://github.com/sass/dart-sass
Expand Down
42 changes: 42 additions & 0 deletions test/node_api/importer_test.dart
Expand Up @@ -556,6 +556,48 @@ void main() {
}))));
});
});

group("includes a fromImport field that's", () {
test("true for an @import", () {
renderSync(RenderOptions(
data: '@import "foo"',
importer: allowInteropCaptureThis(
expectAsync3((RenderContext this_, _, __) {
expect(this_.fromImport, isTrue);
return NodeImporterResult(contents: '');
}))));
});

test("false for a @use", () {
renderSync(RenderOptions(
data: '@use "foo"',
importer: allowInteropCaptureThis(
expectAsync3((RenderContext this_, _, __) {
expect(this_.fromImport, isFalse);
return NodeImporterResult(contents: '');
}))));
});

test("false for a @forward", () {
renderSync(RenderOptions(
data: '@forward "foo"',
importer: allowInteropCaptureThis(
expectAsync3((RenderContext this_, _, __) {
expect(this_.fromImport, isFalse);
return NodeImporterResult(contents: '');
}))));
});

test("false for meta.load-css()", () {
renderSync(RenderOptions(
data: '@use "sass:meta"; @include meta.load-css("foo")',
importer: allowInteropCaptureThis(
expectAsync3((RenderContext this_, _, __) {
expect(this_.fromImport, isFalse);
return NodeImporterResult(contents: '');
}))));
});
});
});

group("gracefully handles an error when", () {
Expand Down

0 comments on commit 8a344e3

Please sign in to comment.