Represent DynamicImport.url as a String (#250)

This works around dart-lang/sdk#32490. We need to preserve the leading
"./" to match Node Sass's behavior.

Closes #246
This commit is contained in:
Natalie Weizenbaum 2018-03-11 15:46:59 -07:00 committed by GitHub
parent 24635614df
commit 07b02174e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 63 additions and 39 deletions

View File

@ -3,6 +3,12 @@
* Add support for importing an `_index.scss` or `_index.sass` file when
importing a directory.
### Node JS API
* Import URLs passed to importers are no longer normalized. For example, if a
stylesheet contains `@import "./foo.scss"`, importers will now receive
`"./foo.scss"` rather than `"foo.scss"`.
## 1.0.0-beta.5.3
* Support hard tabs in the indented syntax.

View File

@ -9,14 +9,16 @@ import '../import.dart';
/// An import that will load a Sass file at runtime.
class DynamicImport implements Import {
// TODO(nweiz): Make this a [Url] when dart-lang/sdk#32490 is fixed, or when
// Node Sass imports no longer expose a leading `./`.
/// The URI of the file to import.
///
/// If this is relative, it's relative to the containing file.
final Uri url;
final String url;
final FileSpan span;
DynamicImport(this.url, this.span);
String toString() => StringExpression.quoteText(url.toString());
String toString() => StringExpression.quoteText(url);
}

View File

@ -58,18 +58,18 @@ class NodeImporter {
/// The [previous] URL is the URL of the stylesheet in which the import
/// appeared. Returns the contents of the stylesheet and the URL to use as
/// [previous] for imports within the loaded stylesheet.
Tuple2<String, Uri> load(Uri url, Uri previous) {
if (url.scheme == '' || url.scheme == 'file') {
var result = _resolvePath(p.fromUri(url), previous);
Tuple2<String, String> load(String url, Uri previous) {
var parsed = Uri.parse(url);
if (parsed.scheme == '' || parsed.scheme == 'file') {
var result = _resolvePath(p.fromUri(parsed), previous);
if (result != null) return result;
}
// The previous URL is always an absolute file path for filesystem imports.
var urlString = url.toString();
var previousString =
previous.scheme == 'file' ? p.fromUri(previous) : previous.toString();
for (var importer in _importers) {
var value = call2(importer, _context, urlString, previousString);
var value = call2(importer, _context, url, previousString);
if (value != null) return _handleImportResult(url, previous, value);
}
@ -81,18 +81,18 @@ class NodeImporter {
/// The [previous] URL is the URL of the stylesheet in which the import
/// appeared. Returns the contents of the stylesheet and the URL to use as
/// [previous] for imports within the loaded stylesheet.
Future<Tuple2<String, Uri>> loadAsync(Uri url, Uri previous) async {
if (url.scheme == '' || url.scheme == 'file') {
var result = _resolvePath(p.fromUri(url), previous);
Future<Tuple2<String, String>> loadAsync(String url, Uri previous) async {
var parsed = Uri.parse(url);
if (parsed.scheme == '' || parsed.scheme == 'file') {
var result = _resolvePath(p.fromUri(parsed), previous);
if (result != null) return result;
}
// The previous URL is always an absolute file path for filesystem imports.
var urlString = url.toString();
var previousString =
previous.scheme == 'file' ? p.fromUri(previous) : previous.toString();
for (var importer in _importers) {
var value = await _callImporterAsync(importer, urlString, previousString);
var value = await _callImporterAsync(importer, url, previousString);
if (value != null) return _handleImportResult(url, previous, value);
}
@ -104,7 +104,7 @@ class NodeImporter {
///
/// Returns the stylesheet at that path and the URL used to load it, or `null`
/// if loading failed.
Tuple2<String, Uri> _resolvePath(String path, Uri previous) {
Tuple2<String, String> _resolvePath(String path, Uri previous) {
if (p.isAbsolute(path)) return _tryPath(path);
// 1: Filesystem imports relative to the base file.
@ -130,16 +130,17 @@ class NodeImporter {
///
/// Returns the stylesheet at that path and the URL used to load it, or `null`
/// if loading failed.
Tuple2<String, Uri> _tryPath(String path) {
Tuple2<String, String> _tryPath(String path) {
var resolved = resolveImportPath(path);
return resolved == null
? null
: new Tuple2(readFile(resolved), p.toUri(resolved));
: new Tuple2(readFile(resolved), p.toUri(resolved).toString());
}
/// Converts an [_Importer]'s return [value] to a tuple that can be returned
/// by [load].
Tuple2<String, Uri> _handleImportResult(Uri url, Uri previous, Object value) {
Tuple2<String, String> _handleImportResult(
String url, Uri previous, Object value) {
if (isJSError(value)) throw value;
NodeImporterResult result;
@ -163,10 +164,10 @@ class NodeImporter {
/// Calls an importer that may or may not be asynchronous.
Future<Object> _callImporterAsync(
_Importer importer, String urlString, String previousString) async {
_Importer importer, String url, String previousString) async {
var completer = new Completer();
var result = call3(importer, _context, urlString, previousString,
var result = call3(importer, _context, url, previousString,
allowInterop(completer.complete));
if (isUndefined(result)) return await completer.future;
return result;

View File

@ -12,7 +12,7 @@ class NodeImporter {
NodeImporter(Object context, Iterable<String> includePaths,
Iterable<_Importer> importers);
Tuple2<String, Uri> load(Uri url, Uri previous) => null;
Tuple2<String, String> load(String url, Uri previous) => null;
Future<Tuple2<String, Uri>> loadAsync(Uri url, Uri previous) => null;
Future<Tuple2<String, String>> loadAsync(String url, Uri previous) => null;
}

View File

@ -762,12 +762,14 @@ abstract class StylesheetParser extends Parser {
/// Parses [url] as an import URL.
@protected
Uri parseImportUrl(String url) {
String parseImportUrl(String url) {
// Backwards-compatibility for implementations that allow absolute Windows
// paths in imports.
if (p.windows.isAbsolute(url)) return p.windows.toUri(url);
if (p.windows.isAbsolute(url)) return p.windows.toUri(url).toString();
return Uri.parse(url);
// Throw a [FormatException] if [url] is invalid.
Uri.parse(url);
return url;
}
/// Returns whether [url] indicates that an `@import` is a plain CSS import.

View File

@ -696,21 +696,23 @@ class _EvaluateVisitor
var stylesheet = await _importLikeNode(import);
if (stylesheet != null) return new Tuple2(null, stylesheet);
} else {
var url = Uri.parse(import.url);
// Try to resolve [import.url] relative to the current URL with the
// current importer.
if (import.url.scheme.isEmpty && _importer != null) {
if (url.scheme.isEmpty && _importer != null) {
var stylesheet =
await _tryImport(_importer, _baseUrl.resolveUri(import.url));
await _tryImport(_importer, _baseUrl.resolveUri(url));
if (stylesheet != null) return new Tuple2(_importer, stylesheet);
}
for (var importer in _importers) {
var stylesheet = await _tryImport(importer, import.url);
var stylesheet = await _tryImport(importer, url);
if (stylesheet != null) return new Tuple2(importer, stylesheet);
}
}
if (import.url.scheme == 'package') {
if (import.url.startsWith('package:')) {
// Special-case this error message, since it's tripped people up in the
// past.
throw "\"package:\" URLs aren't supported on this platform.";
@ -744,13 +746,13 @@ class _EvaluateVisitor
var contents = result.item1;
var url = result.item2;
if (url.scheme == 'file') {
if (url.startsWith('file:')) {
_includedFiles.add(p.fromUri(url));
} else {
_includedFiles.add(url.toString());
_includedFiles.add(url);
}
return url.scheme == 'file' && pUrl.extension(url.path) == '.sass'
return url.startsWith('file') && pUrl.extension(url) == '.sass'
? new Stylesheet.parseSass(contents, url: url, color: _color)
: new Stylesheet.parseScss(contents, url: url, color: _color);
}

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/synchronize.dart for details.
//
// Checksum: 199b3bd52a5c5c147444ee45c2f3d46ef8af2d28
// Checksum: 53f8eb4f4e6ed2cad3fe359490a50e12911a278a
import 'dart:math' as math;
@ -690,21 +690,22 @@ class _EvaluateVisitor
var stylesheet = _importLikeNode(import);
if (stylesheet != null) return new Tuple2(null, stylesheet);
} else {
var url = Uri.parse(import.url);
// Try to resolve [import.url] relative to the current URL with the
// current importer.
if (import.url.scheme.isEmpty && _importer != null) {
var stylesheet =
_tryImport(_importer, _baseUrl.resolveUri(import.url));
if (url.scheme.isEmpty && _importer != null) {
var stylesheet = _tryImport(_importer, _baseUrl.resolveUri(url));
if (stylesheet != null) return new Tuple2(_importer, stylesheet);
}
for (var importer in _importers) {
var stylesheet = _tryImport(importer, import.url);
var stylesheet = _tryImport(importer, url);
if (stylesheet != null) return new Tuple2(importer, stylesheet);
}
}
if (import.url.scheme == 'package') {
if (import.url.startsWith('package:')) {
// Special-case this error message, since it's tripped people up in the
// past.
throw "\"package:\" URLs aren't supported on this platform.";
@ -738,13 +739,13 @@ class _EvaluateVisitor
var contents = result.item1;
var url = result.item2;
if (url.scheme == 'file') {
if (url.startsWith('file:')) {
_includedFiles.add(p.fromUri(url));
} else {
_includedFiles.add(url.toString());
_includedFiles.add(url);
}
return url.scheme == 'file' && pUrl.extension(url.path) == '.sass'
return url.startsWith('file') && pUrl.extension(url) == '.sass'
? new Stylesheet.parseSass(contents, url: url, color: _color)
: new Stylesheet.parseScss(contents, url: url, color: _color);
}

View File

@ -266,6 +266,16 @@ void main() {
}))));
});
// Regression test for #246.
test("doesn't remove ./", () {
renderSync(new RenderOptions(
data: "@import './foo'",
importer: allowInterop(expectAsync2((url, _) {
expect(url, equals('./foo'));
return new NodeImporterResult(contents: '');
}))));
});
test("isn't resolved relative to the current file", () {
renderSync(new RenderOptions(
data: "@import 'foo/bar'",