diff --git a/lib/sass.dart b/lib/sass.dart index 3f5dbc1d..134163b5 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -5,7 +5,7 @@ import 'package:path/path.dart' as p; import 'src/ast/sass.dart'; -import 'src/exception.dart'; +import 'src/sync_package_resolver.dart'; import 'src/utils.dart'; import 'src/visitor/perform.dart'; import 'src/visitor/serialize.dart'; @@ -14,13 +14,21 @@ import 'src/visitor/serialize.dart'; /// /// If [color] is `true`, this will use terminal colors in warnings. /// -/// Throws a [SassException] if conversion fails. -String render(String path, {bool color: false}) { +/// If [packageResolver] is provided, it's used to resolve `package:` imports. +/// Otherwise, they aren't supported. It takes a [SyncPackageResolver][] from +/// the `package_resolver` package. +/// +/// [SyncPackageResolver]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html +/// +/// Finally throws a [SassException] if conversion fails. +String render(String path, + {bool color: false, SyncPackageResolver packageResolver}) { var contents = readSassFile(path); var url = p.toUri(path); var sassTree = p.extension(path) == '.sass' ? new Stylesheet.parseSass(contents, url: url, color: color) : new Stylesheet.parseScss(contents, url: url, color: color); - var cssTree = evaluate(sassTree, color: color); + var cssTree = + evaluate(sassTree, color: color, packageResolver: packageResolver); return toCss(cssTree); } diff --git a/lib/src/io/interface.dart b/lib/src/io/interface.dart index 09d53569..86b942a3 100644 --- a/lib/src/io/interface.dart +++ b/lib/src/io/interface.dart @@ -37,5 +37,8 @@ String readFileAsString(String path) => null; /// Returns whether a file at [path] exists. bool fileExists(String path) => null; +/// Returns whether a dir at [path] exists. +bool dirExists(String path) => null; + /// Gets and sets the exit code that the process will use when it exits. int exitCode; diff --git a/lib/src/io/node.dart b/lib/src/io/node.dart index 8b0454a8..fc36e64d 100644 --- a/lib/src/io/node.dart +++ b/lib/src/io/node.dart @@ -74,6 +74,8 @@ String _cleanErrorMessage(_SystemError error) { bool fileExists(String path) => _fs.existsSync(path); +bool dirExists(String path) => _fs.existsSync(path); + @JS("process.stderr") external _Stderr get _stderr; diff --git a/lib/src/io/vm.dart b/lib/src/io/vm.dart index a8bf7516..1dcaef0b 100644 --- a/lib/src/io/vm.dart +++ b/lib/src/io/vm.dart @@ -15,3 +15,5 @@ List readFileAsBytes(String path) => new io.File(path).readAsBytesSync(); String readFileAsString(String path) => new io.File(path).readAsStringSync(); bool fileExists(String path) => new io.File(path).existsSync(); + +bool dirExists(String path) => new io.Directory(path).existsSync(); diff --git a/lib/src/sync_package_resolver.dart b/lib/src/sync_package_resolver.dart new file mode 100644 index 00000000..163b2eb3 --- /dev/null +++ b/lib/src/sync_package_resolver.dart @@ -0,0 +1,2 @@ +export 'package:package_resolver/package_resolver.dart' + if (node) 'sync_package_resolver/node.dart'; diff --git a/lib/src/sync_package_resolver/node.dart b/lib/src/sync_package_resolver/node.dart new file mode 100644 index 00000000..f4339160 --- /dev/null +++ b/lib/src/sync_package_resolver/node.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +class SyncPackageResolver { + static final _error = + new UnsupportedError('SyncPackageResolver is not supported in JS.'); + + static Future get current => throw _error; + + Uri resolveUri(packageUri) => throw _error; + + factory SyncPackageResolver.config(Map configMap) => + throw _error; +} diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index abb0ee42..796fe30d 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -21,6 +21,7 @@ import '../exception.dart'; import '../extend/extender.dart'; import '../io.dart'; import '../parse/keyframe_selector.dart'; +import '../sync_package_resolver.dart'; import '../utils.dart'; import '../value.dart'; import 'interface/statement.dart'; @@ -43,9 +44,13 @@ typedef _ScopeCallback(callback()); CssStylesheet evaluate(Stylesheet stylesheet, {Iterable loadPaths, Environment environment, - bool color: false}) => + bool color: false, + SyncPackageResolver packageResolver}) => new _PerformVisitor( - loadPaths: loadPaths, environment: environment, color: color) + loadPaths: loadPaths, + environment: environment, + color: color, + packageResolver: packageResolver) .run(stylesheet); /// A visitor that executes Sass code to produce a CSS tree. @@ -131,11 +136,18 @@ class _PerformVisitor /// invocations, and imports surrounding the current context. final _stack = []; + /// The resolver to use for `package:` URLs, or `null` if no resolver exists. + final SyncPackageResolver _packageResolver; + _PerformVisitor( - {Iterable loadPaths, Environment environment, bool color: false}) + {Iterable loadPaths, + Environment environment, + bool color: false, + SyncPackageResolver packageResolver}) : _loadPaths = loadPaths == null ? const [] : new List.from(loadPaths), _environment = environment ?? new Environment(), - _color = color { + _color = color, + _packageResolver = packageResolver { _environment.defineFunction("variable-exists", r"$name", (arguments) { var variable = arguments[0].assertString("name"); return new SassBoolean(_environment.variableExists(variable.text)); @@ -581,11 +593,27 @@ class _PerformVisitor _activeImports.remove(url); } + /// Returns [import]'s URL, resolved to a `file:` URL if possible. + Uri _resolveImportUrl(DynamicImport import) { + var packageUrl = import.url; + if (packageUrl.scheme != 'package') return packageUrl; + + if (_packageResolver == null) { + throw _exception( + '"package:" URLs aren\'t supported on this platform.', import.span); + } + + var resolvedPackageUrl = _packageResolver.resolveUri(packageUrl); + if (resolvedPackageUrl != null) return resolvedPackageUrl; + + throw _exception("Unknown package.", import.span); + } + /// Loads the [Stylesheet] imported by [import], or throws a /// [SassRuntimeException] if loading fails. Stylesheet _loadImport(DynamicImport import) { var path = _importPaths.putIfAbsent(import, () { - var path = p.fromUri(import.url); + var path = p.fromUri(_resolveImportUrl(import)); var extension = p.extension(path); var tryPath = extension == '.sass' || extension == '.scss' ? _tryImportPath diff --git a/pubspec.yaml b/pubspec.yaml index 411c0ef8..c73d3c1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: string_scanner: ">=0.1.5 <2.0.0" stack_trace: ">=0.9.0 <2.0.0" tuple: "^1.0.0" + package_resolver: ^1.0.0 dev_dependencies: archive: "^1.0.0" diff --git a/test/cli_shared.dart b/test/cli_shared.dart index c93fe969..d2c249e4 100644 --- a/test/cli_shared.dart +++ b/test/cli_shared.dart @@ -95,4 +95,17 @@ void sharedTests(ScheduledProcess runSass(List arguments)) { sass.stderr.expect(consumeThrough(contains("\.dart"))); sass.shouldExit(65); }); + + test("fails to import a package url", () { + d.file("test.scss", "@import 'package:nope/test';").create(); + + var sass = runSass(["test.scss", "test.css"]); + sass.stderr.expect(inOrder([ + "Error: \"package:\" URLs aren't supported on this platform.", + "@import 'package:nope/test';", + " ^^^^^^^^^^^^^^^^^^^", + " test.scss 1:9 root stylesheet" + ])); + sass.shouldExit(65); + }); } diff --git a/test/dart_script_test.dart b/test/dart_script_test.dart new file mode 100644 index 00000000..eec79f64 --- /dev/null +++ b/test/dart_script_test.dart @@ -0,0 +1,40 @@ +// Copyright 2017 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:package_resolver/package_resolver.dart'; +import 'package:path/path.dart' as p; +import 'package:scheduled_test/descriptor.dart' as d; +import 'package:scheduled_test/scheduled_test.dart'; + +import 'package:sass/sass.dart'; +import 'package:sass/src/exception.dart'; + +import 'utils.dart'; + +main() { + useSandbox(); + + test("successfully imports a package URL", () { + d.dir("subdir", [d.file("test.scss", "a {b: 1 + 2}")]).create(); + + d.file("test.scss", '@import "package:fake_package/test";').create(); + var resolver = new SyncPackageResolver.config( + {"fake_package": p.toUri(p.join(sandbox, 'subdir'))}); + + schedule(() { + var css = render(p.join(sandbox, "test.scss"), packageResolver: resolver); + expect(css, equals("a {\n b: 3;\n}")); + }); + }); + + test("imports a package URL from a missing package", () { + d.file("test.scss", '@import "package:fake_package/test_aux";').create(); + var resolver = new SyncPackageResolver.config({}); + + schedule(() { + expect(() => render(sandbox + "/test.scss", packageResolver: resolver), + throwsA(new isInstanceOf())); + }); + }); +}