Expose a Dart API for importers

Closes #172
This commit is contained in:
Natalie Weizenbaum 2017-10-05 18:45:26 -07:00 committed by Natalie Weizenbaum
parent ed1d6ef6b1
commit a003e5c31f
5 changed files with 354 additions and 47 deletions

View File

@ -5,6 +5,22 @@
* Don't crash when evaluating CSS variables whose names are entirely
interpolated (for example, `#{--foo}: ...`).
### Dart API
* Added an `Importer` class. This can be extended by users to provide support
for custom resolution for `@import` rules.
* Added built-in `FilesystemImporter` and `PackageImporter` implementations that
support resolving `file:` and `package:` URLs, respectively.
* Added an `importers` argument to the `compile()` and `compileString()`
functions that provides `Importer`s to use when resolving `@import` rules.
* Added a `loadPaths` argument to the `compile()` and `compileString()`
functions that provides paths to search for stylesheets when resolving
`@import` rules. This is a shorthand for passing `FilesystemImporter`s to the
`importers` argument.
## 1.0.0-beta.2
* Add support for the `::slotted()` pseudo-element.

View File

@ -4,22 +4,46 @@
import 'src/compile.dart' as c;
import 'src/exception.dart';
import 'src/importer.dart';
import 'src/sync_package_resolver.dart';
export 'src/importer.dart';
export 'src/importer/filesystem.dart';
export 'src/importer/package.dart';
export 'src/importer/result.dart';
/// Loads the Sass file at [path], compiles it to CSS, and returns the result.
///
/// If [color] is `true`, this will use terminal colors in warnings.
///
/// 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.
/// Imports are resolved by trying, in order:
///
/// * Loading a file relative to [path].
///
/// * Each importer in [importers].
///
/// * Each load path in [loadPaths]. Note that this is a shorthand for adding
/// [FilesystemImporter]s to [importers].
///
/// * `package:` resolution using [packageResolver], which is a
/// [SyncPackageResolver][] from the `package_resolver` package. Note that
/// this is a shorthand for adding a [PackageImporter] to [importers].
///
/// [SyncPackageResolver]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html
///
/// Throws a [SassException] if conversion fails.
String compile(String path,
{bool color: false, SyncPackageResolver packageResolver}) =>
c.compile(path, color: color, packageResolver: packageResolver).css;
{bool color: false,
Iterable<Importer> importers,
Iterable<String> loadPaths,
SyncPackageResolver packageResolver}) {
var result = c.compile(path,
color: color,
importers: importers,
loadPaths: loadPaths,
packageResolver: packageResolver);
return result.css;
}
/// Compiles [source] to CSS and returns the result.
///
@ -27,25 +51,41 @@ String compile(String path,
/// otherwise (and by default) it uses SCSS. If [color] is `true`, this will use
/// terminal colors in warnings.
///
/// 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.
/// Imports are resolved by trying, in order:
///
/// * The given [importer], with the imported URL resolved relative to [url].
///
/// * Each importer in [importers].
///
/// * Each load path in [loadPaths]. Note that this is a shorthand for adding
/// [FilesystemImporter]s to [importers].
///
/// * `package:` resolution using [packageResolver], which is a
/// [SyncPackageResolver][] from the `package_resolver` package. Note that
/// this is a shorthand for adding a [PackageImporter] to [importers].
///
/// [SyncPackageResolver]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html
///
/// The [url] indicates the location from which [source] was loaded. It may may
/// be a [String] or a [Uri].
/// The [url] indicates the location from which [source] was loaded. It may be a
/// [String] or a [Uri]. If [importer] is passed, [url] must be passed as well
/// and `importer.load(url)` should return `source`.
///
/// Throws a [SassException] if conversion fails.
String compileString(String source,
{bool indented: false,
bool color: false,
Iterable<Importer> importers,
Iterable<String> loadPaths,
SyncPackageResolver packageResolver,
Importer importer,
url}) {
var result = c.compileString(source,
indented: indented,
color: color,
importers: importers,
loadPaths: loadPaths,
packageResolver: packageResolver,
importer: importer,
url: url);
return result.css;
}

View File

@ -0,0 +1,113 @@
// 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.
@TestOn('vm')
import 'package:test/test.dart';
import 'package:sass/sass.dart';
import 'package:sass/src/exception.dart';
main() {
test("uses an importer to resolve an @import", () {
var css = compileString('@import "orange";', importers: [
new _TestImporter((url) => url, (url) {
return new ImporterResult('.$url {color: $url}', indented: false);
})
]);
expect(css, equals(".orange {\n color: orange;\n}"));
});
test("passes the canonicalized URL to the importer", () {
var css = compileString('@import "orange";', importers: [
new _TestImporter((url) => new Uri(path: 'blue'), (url) {
return new ImporterResult('.$url {color: $url}', indented: false);
})
]);
expect(css, equals(".blue {\n color: blue;\n}"));
});
test("only invokes the importer once for a given canonicalization", () {
var css = compileString("""
@import "orange";
@import "orange";
""", importers: [
new _TestImporter(
(url) => new Uri(path: 'blue'),
expectAsync1((url) {
return new ImporterResult('.$url {color: $url}', indented: false);
}, count: 1))
]);
expect(css, equals("""
.blue {
color: blue;
}
.blue {
color: blue;
}"""));
});
test("wraps an error in canonicalize()", () {
expect(() {
compileString('@import "orange";', importers: [
new _TestImporter((url) {
throw "this import is bad actually";
}, expectAsync1((_) => null, count: 0))
]);
}, throwsA(predicate((error) {
expect(error, new isInstanceOf<SassException>());
expect(
error.toString(), startsWith("Error: this import is bad actually"));
return true;
})));
});
test("wraps an error in load()", () {
expect(() {
compileString('@import "orange";', importers: [
new _TestImporter((url) => url, (url) {
throw "this import is bad actually";
})
]);
}, throwsA(predicate((error) {
expect(error, new isInstanceOf<SassException>());
expect(
error.toString(), startsWith("Error: this import is bad actually"));
return true;
})));
});
test("prefers .message to .toString() for an importer error", () {
expect(() {
compileString('@import "orange";', importers: [
new _TestImporter((url) => url, (url) {
throw new FormatException("bad format somehow");
})
]);
}, throwsA(predicate((error) {
expect(error, new isInstanceOf<SassException>());
// FormatException.toString() starts with "FormatException:", but
// the error message should not.
expect(error.toString(), startsWith("Error: bad format somehow"));
return true;
})));
});
}
/// An [Importer] whose [canonicalize] and [load] methods are provided by
/// closures.
class _TestImporter extends Importer {
final Uri Function(Uri url) _canonicalize;
final ImporterResult Function(Uri url) _load;
_TestImporter(this._canonicalize, this._load);
Uri canonicalize(Uri url) => _canonicalize(url);
ImporterResult load(Uri url) => _load(url);
}

175
test/dart_api_test.dart Normal file
View File

@ -0,0 +1,175 @@
// 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.
@TestOn('vm')
import 'package:package_resolver/package_resolver.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:sass/sass.dart';
import 'package:sass/src/exception.dart';
import 'package:sass/src/util/path.dart';
main() {
group("importers", () {
test("is used to resolve imports", () async {
await d.dir("subdir", [d.file("subtest.scss", "a {b: c}")]).create();
await d.file("test.scss", '@import "subtest.scss";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
importers: [new FilesystemImporter(p.join(d.sandbox, 'subdir'))]);
expect(css, equals("a {\n b: c;\n}"));
});
test("are checked in order", () async {
await d
.dir("first", [d.file("other.scss", "a {b: from-first}")]).create();
await d
.dir("second", [d.file("other.scss", "a {b: from-second}")]).create();
await d.file("test.scss", '@import "other";').create();
var css = compile(p.join(d.sandbox, "test.scss"), importers: [
new FilesystemImporter(p.join(d.sandbox, 'first')),
new FilesystemImporter(p.join(d.sandbox, 'second'))
]);
expect(css, equals("a {\n b: from-first;\n}"));
});
});
group("loadPaths", () {
test("is used to import file: URLs", () async {
await d.dir("subdir", [d.file("subtest.scss", "a {b: c}")]).create();
await d.file("test.scss", '@import "subtest.scss";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
loadPaths: [p.join(d.sandbox, 'subdir')]);
expect(css, equals("a {\n b: c;\n}"));
});
test("can import partials", () async {
await d.dir("subdir", [d.file("_subtest.scss", "a {b: c}")]).create();
await d.file("test.scss", '@import "subtest.scss";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
loadPaths: [p.join(d.sandbox, 'subdir')]);
expect(css, equals("a {\n b: c;\n}"));
});
test("adds a .scss extension", () async {
await d.dir("subdir", [d.file("subtest.scss", "a {b: c}")]).create();
await d.file("test.scss", '@import "subtest";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
loadPaths: [p.join(d.sandbox, 'subdir')]);
expect(css, equals("a {\n b: c;\n}"));
});
test("adds a .sass extension", () async {
await d.dir("subdir", [d.file("subtest.sass", "a\n b: c")]).create();
await d.file("test.scss", '@import "subtest";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
loadPaths: [p.join(d.sandbox, 'subdir')]);
expect(css, equals("a {\n b: c;\n}"));
});
test("are checked in order", () async {
await d
.dir("first", [d.file("other.scss", "a {b: from-first}")]).create();
await d
.dir("second", [d.file("other.scss", "a {b: from-second}")]).create();
await d.file("test.scss", '@import "other";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
loadPaths: [p.join(d.sandbox, 'first'), p.join(d.sandbox, 'second')]);
expect(css, equals("a {\n b: from-first;\n}"));
});
});
group("packageResolver", () {
test("is used to import package: URLs", () async {
await d.dir("subdir", [d.file("test.scss", "a {b: 1 + 2}")]).create();
await d
.file("test.scss", '@import "package:fake_package/test";')
.create();
var resolver = new SyncPackageResolver.config(
{"fake_package": p.toUri(p.join(d.sandbox, 'subdir'))});
var css =
compile(p.join(d.sandbox, "test.scss"), packageResolver: resolver);
expect(css, equals("a {\n b: 3;\n}"));
});
test("doesn't import a package URL from a missing package", () async {
await d
.file("test.scss", '@import "package:fake_package/test_aux";')
.create();
var resolver = new SyncPackageResolver.config({});
expect(() => compile(d.sandbox + "/test.scss", packageResolver: resolver),
throwsA(new isInstanceOf<SassRuntimeException>()));
});
});
group("import precedence", () {
test("relative imports take precedence over importers", () async {
await d.dir(
"subdir", [d.file("other.scss", "a {b: from-load-path}")]).create();
await d.file("other.scss", "a {b: from-relative}").create();
await d.file("test.scss", '@import "other";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
importers: [new FilesystemImporter(p.join(d.sandbox, 'subdir'))]);
expect(css, equals("a {\n b: from-relative;\n}"));
});
test("the original importer takes precedence over other importers",
() async {
await d.dir(
"original", [d.file("other.scss", "a {b: from-original}")]).create();
await d
.dir("other", [d.file("other.scss", "a {b: from-other}")]).create();
var css = compileString('@import "other";',
importer: new FilesystemImporter(p.join(d.sandbox, 'original')),
url: p.toUri(p.join(d.sandbox, 'original', 'test.scss')),
importers: [new FilesystemImporter(p.join(d.sandbox, 'other'))]);
expect(css, equals("a {\n b: from-original;\n}"));
});
test("importers take precedence over load paths", () async {
await d.dir("load-path",
[d.file("other.scss", "a {b: from-load-path}")]).create();
await d.dir(
"importer", [d.file("other.scss", "a {b: from-importer}")]).create();
await d.file("test.scss", '@import "other";').create();
var css = compile(p.join(d.sandbox, "test.scss"),
importers: [new FilesystemImporter(p.join(d.sandbox, 'importer'))],
loadPaths: [p.join(d.sandbox, 'load-path')]);
expect(css, equals("a {\n b: from-importer;\n}"));
});
test("importers take precedence over packageResolver", () async {
await d.dir("package",
[d.file("other.scss", "a {b: from-package-resolver}")]).create();
await d.dir(
"importer", [d.file("other.scss", "a {b: from-importer}")]).create();
await d
.file("test.scss", '@import "package:fake_package/other";')
.create();
var css = compile(p.join(d.sandbox, "test.scss"),
importers: [
new PackageImporter(new SyncPackageResolver.config(
{"fake_package": p.toUri(p.join(d.sandbox, 'importer'))}))
],
packageResolver: new SyncPackageResolver.config(
{"fake_package": p.toUri(p.join(d.sandbox, 'package'))}));
expect(css, equals("a {\n b: from-importer;\n}"));
});
});
}

View File

@ -1,37 +0,0 @@
// 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.
@TestOn('vm')
import 'package:package_resolver/package_resolver.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:sass/sass.dart';
import 'package:sass/src/exception.dart';
import 'package:sass/src/util/path.dart';
main() {
test("successfully imports a package URL", () async {
await d.dir("subdir", [d.file("test.scss", "a {b: 1 + 2}")]).create();
await d.file("test.scss", '@import "package:fake_package/test";').create();
var resolver = new SyncPackageResolver.config(
{"fake_package": p.toUri(p.join(d.sandbox, 'subdir'))});
var css =
compile(p.join(d.sandbox, "test.scss"), packageResolver: resolver);
expect(css, equals("a {\n b: 3;\n}"));
});
test("imports a package URL from a missing package", () async {
await d
.file("test.scss", '@import "package:fake_package/test_aux";')
.create();
var resolver = new SyncPackageResolver.config({});
expect(() => compile(d.sandbox + "/test.scss", packageResolver: resolver),
throwsA(new isInstanceOf<SassRuntimeException>()));
});
}