From ed1d6ef6b176555d519cfe4297c1ce05b7b2f343 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 5 Oct 2017 17:25:14 -0700 Subject: [PATCH] Add importer infrastructure This isn't yet exposed by any public-facing API. --- lib/src/compile.dart | 34 ++++- lib/src/executable.dart | 11 +- lib/src/importer.dart | 66 +++++++++ lib/src/importer/filesystem.dart | 51 +++++++ lib/src/importer/no_op.dart | 17 +++ lib/src/importer/package.dart | 44 ++++++ lib/src/importer/result.dart | 37 +++++ lib/src/node.dart | 2 +- lib/src/visitor/evaluate.dart | 224 +++++++++++++++++-------------- pubspec.yaml | 1 + 10 files changed, 374 insertions(+), 113 deletions(-) create mode 100644 lib/src/importer.dart create mode 100644 lib/src/importer/filesystem.dart create mode 100644 lib/src/importer/no_op.dart create mode 100644 lib/src/importer/package.dart create mode 100644 lib/src/importer/result.dart diff --git a/lib/src/compile.dart b/lib/src/compile.dart index d4b9f4f4..c407b3ad 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -3,6 +3,9 @@ // https://opensource.org/licenses/MIT. import 'ast/sass.dart'; +import 'importer.dart'; +import 'importer/filesystem.dart'; +import 'importer/package.dart'; import 'io.dart'; import 'sync_package_resolver.dart'; import 'util/path.dart'; @@ -14,6 +17,7 @@ import 'visitor/serialize.dart'; CompileResult compile(String path, {bool indented, bool color: false, + Iterable importers, SyncPackageResolver packageResolver, Iterable loadPaths, OutputStyle style, @@ -23,9 +27,11 @@ CompileResult compile(String path, compileString(readFile(path), indented: indented ?? p.extension(path) == '.sass', color: color, + importers: importers, packageResolver: packageResolver, - style: style, loadPaths: loadPaths, + importer: new FilesystemImporter('.'), + style: style, useSpaces: useSpaces, indentWidth: indentWidth, lineFeed: lineFeed, @@ -36,8 +42,10 @@ CompileResult compile(String path, CompileResult compileString(String source, {bool indented: false, bool color: false, + Iterable importers, SyncPackageResolver packageResolver, Iterable loadPaths, + Importer importer, OutputStyle style, bool useSpaces: true, int indentWidth, @@ -46,15 +54,24 @@ CompileResult compileString(String source, var sassTree = indented ? new Stylesheet.parseSass(source, url: url, color: color) : new Stylesheet.parseScss(source, url: url, color: color); + + var importerList = (importers?.toList() ?? []); + if (loadPaths != null) { + importerList.addAll(loadPaths.map((path) => new FilesystemImporter(path))); + } + if (packageResolver != null) { + importerList.add(new PackageImporter(packageResolver)); + } + var evaluateResult = evaluate(sassTree, - color: color, packageResolver: packageResolver, loadPaths: loadPaths); + importers: importerList, importer: importer, color: color); var css = serialize(evaluateResult.stylesheet, style: style, useSpaces: useSpaces, indentWidth: indentWidth, lineFeed: lineFeed); - return new CompileResult(css, evaluateResult.includedUrls); + return new CompileResult(css, evaluateResult.includedFiles); } /// The result of compiling a Sass document to CSS, along with metadata about @@ -63,9 +80,12 @@ class CompileResult { /// The compiled CSS. final String css; - /// The URLs that were loaded during the compilation, including the main - /// file's. - final Set includedUrls; + /// The set that will eventually populate the JS API's + /// `result.stats.includedFiles` field. + /// + /// For filesystem imports, this contains the import path. For all other + /// imports, it contains the URL passed to the `@import`. + final Set includedFiles; - CompileResult(this.css, this.includedUrls); + CompileResult(this.css, this.includedFiles); } diff --git a/lib/src/executable.dart b/lib/src/executable.dart index a049538e..742a8ca7 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -58,12 +58,10 @@ main(List args) async { try { String css; if (stdinFlag) { - css = compileString(await readStdin(), color: color); + css = await _compileStdin(); } else { var input = options.rest.first; - css = input == '-' - ? compileString(await readStdin(), color: color) - : compile(input, color: color); + css = input == '-' ? await _compileStdin() : compile(input, color: color); } if (css.isNotEmpty) print(css); @@ -124,6 +122,11 @@ Future _loadVersion() async { .last; } +/// Compiles Sass from standard input and returns the result. +Future _compileStdin({bool color: false}) async => + compileString(await readStdin(), + color: color, importer: new FilesystemImporter('.')); + /// Print the usage information for Sass, with [message] as a header. void _printUsage(ArgParser parser, String message) { print("$message\n"); diff --git a/lib/src/importer.dart b/lib/src/importer.dart new file mode 100644 index 00000000..ac1d3107 --- /dev/null +++ b/lib/src/importer.dart @@ -0,0 +1,66 @@ +// 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 'importer/no_op.dart'; +import 'importer/result.dart'; + +/// An interface for importers that resolves URLs in `@import`s to the contents +/// of Sass files. +/// +/// Importers should override [toString] to provide a human-readable description +/// of the importer. For example, the default filesystem importer returns its +/// load path. +/// +/// Subclasses should extend [Importer], not implement it. +abstract class Importer { + /// An importer that never imports any stylesheets. + /// + /// This is used for stylesheets which don't support relative imports, such as + /// those created from Dart code with plain strings. + static final Importer noOp = new NoOpImporter(); + + /// If [url] is recognized by this importer, returns its canonical format. + /// + /// If Sass has already loaded a stylesheet with the returned canonical URL, + /// it re-uses the existing parse tree. This means that importers **must + /// ensure** that the same canonical URL always refers to the same stylesheet, + /// *even across different importers*. + /// + /// This may return `null` if [url] isn't recognized by this importer. + /// + /// If this importer's URL format supports file extensions, it should + /// canonicalize them the same way as the default filesystem importer: + /// + /// * If the [url] ends in `.sass` or `.scss`, the importer should look for + /// a stylesheet with that exact URL and return `null` if it's not found. + /// + /// * Otherwise, the importer should look for a stylesheet at `"$url.sass"` or + /// one at `"$url.scss"`, in that order. If neither is found, it should + /// return `null`. + /// + /// Sass assumes that calling [canonicalize] multiple times with the same URL + /// will return the same result. + Uri canonicalize(Uri url); + + /// Loads the Sass text for the given [url], or returns `null` if + /// this importer can't find the stylesheet it refers to. + /// + /// The [url] comes from a call to [canonicalize] for this importer. + /// + /// When Sass encounters an `@import` rule in a stylesheet, it first calls + /// [canonicalize] and [load] on the importer that first loaded that + /// stylesheet with the imported URL resolved relative to the stylesheet's + /// original URL. If either of those returns `null`, it then calls + /// [canonicalize] and [load] on each importer in order with the URL as it + /// appears in the `@import` rule. + /// + /// If the importer finds a stylesheet at [url] but it fails to load for some + /// reason, or if [url] is uniquely associated with this importer but doesn't + /// refer to a real stylesheet, the importer may throw an exception that will + /// be wrapped by Sass. If the exception object has a `message` property, it + /// will be used as the wrapped exception's message; otherwise, the exception + /// object's `toString()` will be used. This means it's safe for importers to + /// throw plain strings. + ImporterResult load(Uri url); +} diff --git a/lib/src/importer/filesystem.dart b/lib/src/importer/filesystem.dart new file mode 100644 index 00000000..8da7ffda --- /dev/null +++ b/lib/src/importer/filesystem.dart @@ -0,0 +1,51 @@ +// 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:path/path.dart' as p; + +import '../importer.dart'; +import '../io.dart'; +import 'result.dart'; + +/// An importer that loads files from a load path on the filesystem. +class FilesystemImporter extends Importer { + /// The path relative to which this importer looks for files. + final String _loadPath; + + /// Creates an importer that loads files relative to [loadPath]. + FilesystemImporter(this._loadPath); + + Uri canonicalize(Uri url) { + var urlPath = p.fromUri(url); + var path = p.join(_loadPath, urlPath); + var extension = p.extension(path); + var resolved = extension == '.sass' || extension == '.scss' + ? _tryPath(path) + : _tryPathWithExtensions(path); + return resolved == null ? null : p.toUri(p.canonicalize(resolved)); + } + + /// Like [_tryPath], but checks both `.sass` and `.scss` extensions. + String _tryPathWithExtensions(String path) => + _tryPath(path + '.sass') ?? _tryPath(path + '.scss'); + + /// If a file exists at [path], or a partial with the same name exists, + /// returns the resolved path. + /// + /// Otherwise, returns `null`. + String _tryPath(String path) { + var partial = p.join(p.dirname(path), "_${p.basename(path)}"); + if (fileExists(partial)) return partial; + if (fileExists(path)) return path; + return null; + } + + ImporterResult load(Uri url) { + var path = p.fromUri(url); + return new ImporterResult(readFile(path), + sourceMapUrl: url, indented: p.extension(path) == '.sass'); + } + + String toString() => _loadPath; +} diff --git a/lib/src/importer/no_op.dart b/lib/src/importer/no_op.dart new file mode 100644 index 00000000..a671d6e2 --- /dev/null +++ b/lib/src/importer/no_op.dart @@ -0,0 +1,17 @@ +// 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 '../importer.dart'; +import 'result.dart'; + +/// An importer that never imports any stylesheets. +/// +/// This is used for stylesheets which don't support relative imports, such as +/// those created from Dart code with plain strings. +class NoOpImporter extends Importer { + Uri canonicalize(Uri url) => null; + ImporterResult load(Uri url) => null; + + String toString() => "(unknown)"; +} diff --git a/lib/src/importer/package.dart b/lib/src/importer/package.dart new file mode 100644 index 00000000..da8697e5 --- /dev/null +++ b/lib/src/importer/package.dart @@ -0,0 +1,44 @@ +// 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 '../importer.dart'; +import '../sync_package_resolver.dart'; +import 'filesystem.dart'; +import 'result.dart'; + +/// A filesystem importer to use when resolving the results of `package:` URLs. +/// +/// This allows us to avoid duplicating the logic for choosing an extension and +/// looking for partials. +final _filesystemImporter = new FilesystemImporter('.'); + +/// An importer that loads stylesheets from `package:` imports. +class PackageImporter extends Importer { + /// The resolver that converts `package:` imports to `file:`. + final SyncPackageResolver _packageResolver; + + /// Creates an importer that loads stylesheets from `package:` URLs according + /// to [packageResolver], which is a [SyncPackageResolver][] from the + /// `package_resolver` package. + /// + /// [SyncPackageResolver]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html + PackageImporter(this._packageResolver); + + Uri canonicalize(Uri url) { + if (url.scheme != 'package') return null; + + var resolved = _packageResolver.resolveUri(url); + if (resolved == null) throw "Unknown package."; + + if (resolved.scheme.isNotEmpty && resolved.scheme != 'file') { + throw "Unsupported URL ${resolved}."; + } + + return _filesystemImporter.canonicalize(resolved); + } + + ImporterResult load(Uri url) => _filesystemImporter.load(url); + + String toString() => "package:..."; +} diff --git a/lib/src/importer/result.dart b/lib/src/importer/result.dart new file mode 100644 index 00000000..97b73dfb --- /dev/null +++ b/lib/src/importer/result.dart @@ -0,0 +1,37 @@ +// 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 'dart:convert'; + +import 'package:meta/meta.dart'; + +import '../importer.dart'; + +/// The result of importing a Sass stylesheet, as returned by [Importer.load]. +class ImporterResult { + /// The contents of the stylesheet. + final String contents; + + /// An absolute, browser-accessible URL indicating the resolved location of + /// the imported stylesheet. + /// + /// This should be a `file:` URL if one is available, but an `http:` URL is + /// acceptable as well. If no URL is supplied, a `data:` URL is generated + /// automatically from [contents]. + Uri get sourceMapUrl => + _sourceMapUrl ?? new Uri.dataFromString(contents, encoding: UTF8); + final Uri _sourceMapUrl; + + /// Whether the stylesheet uses the indented syntax. + final bool isIndented; + + ImporterResult(this.contents, {Uri sourceMapUrl, @required bool indented}) + : _sourceMapUrl = sourceMapUrl, + isIndented = indented { + if (sourceMapUrl?.scheme == '') { + throw new ArgumentError.value( + sourceMapUrl, 'sourceMapUrl', 'must be absolute'); + } + } +} diff --git a/lib/src/node.dart b/lib/src/node.dart index 4964ecda..e0aac6ac 100644 --- a/lib/src/node.dart +++ b/lib/src/node.dart @@ -103,7 +103,7 @@ RenderResult _doRender(RenderOptions options) { start: start.millisecondsSinceEpoch, end: end.millisecondsSinceEpoch, duration: end.difference(start).inMilliseconds, - includedFiles: result.includedUrls.map((url) => p.fromUri(url)).toList()); + includedFiles: result.includedFiles.toList()); } /// Converts a [SassException] to a [RenderError]. diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 95a0847d..982582cf 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -18,9 +18,10 @@ import '../color_names.dart'; import '../environment.dart'; import '../exception.dart'; import '../extend/extender.dart'; +import '../importer.dart'; +import '../importer/filesystem.dart'; import '../io.dart'; import '../parse/keyframe_selector.dart'; -import '../sync_package_resolver.dart'; import '../utils.dart'; import '../util/path.dart'; import '../value.dart'; @@ -43,24 +44,27 @@ final _noSourceUrl = Uri.parse("-"); /// /// If [color] is `true`, this will use terminal colors in warnings. /// +/// If [importer] is passed, it's used to resolve relative imports in +/// [stylesheet] relative to `stylesheet.span.sourceUrl`. +/// /// Throws a [SassRuntimeException] if evaluation fails. EvaluateResult evaluate(Stylesheet stylesheet, - {Iterable loadPaths, + {Iterable importers, + Importer importer, Environment environment, - bool color: false, - SyncPackageResolver packageResolver}) => + bool color: false}) => new _EvaluateVisitor( - loadPaths: loadPaths, + importers: importers, + importer: importer, environment: environment, - color: color, - packageResolver: packageResolver) + color: color) .run(stylesheet); /// A visitor that executes Sass code to produce a CSS tree. class _EvaluateVisitor implements StatementVisitor, ExpressionVisitor { - /// The paths to search for Sass files being imported. - final List _loadPaths; + /// The importers to use when loading new Sass files. + final List _importers; /// Whether to use terminal colors in warnings. final bool _color; @@ -68,6 +72,15 @@ class _EvaluateVisitor /// The current lexical environment. Environment _environment; + /// The importer that's currently being used to resolve relative imports. + /// + /// If this is `null`, relative imports aren't supported in the current + /// stylesheet. + Importer _importer; + + /// The base URL to use for resolving relative imports. + Uri _baseUrl; + /// The style rule that defines the current parent selector, if any. CssStyleRule _styleRule; @@ -117,18 +130,15 @@ class _EvaluateVisitor /// the stylesheet has been fully performed. var _outOfOrderImports = []; - /// The resolved URLs for each [DynamicImport] that's been seen so far. - /// - /// This is cached in case the same file is imported multiple times, and thus - /// its imports need to be resolved multiple times. - final _importPaths = {}; + /// The parsed stylesheets for each canonicalized import URL. + final _importCache = {}; - /// The parsed stylesheets for each resolved import URL. + /// The set that will eventually populate the JS API's + /// `result.stats.includedFiles` field. /// - /// This is separate from [_importPaths] because multiple `@import` rules may - /// import the same stylesheet, and we don't want to parse the same stylesheet - /// multiple times. - final _importedFiles = {}; + /// For filesystem imports, this contains the import path. For all other + /// imports, it contains the URL passed to the `@import`. + final _includedFiles = new Set(); final _activeImports = new Set(); @@ -139,18 +149,15 @@ class _EvaluateVisitor /// 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; - _EvaluateVisitor( - {Iterable loadPaths, + {Iterable importers, + Importer importer, Environment environment, - bool color: false, - SyncPackageResolver packageResolver}) - : _loadPaths = loadPaths == null ? const [] : new List.from(loadPaths), + bool color: false}) + : _importers = importers == null ? const [] : importers.toList(), + _importer = importer ?? Importer.noOp, _environment = environment ?? new Environment(), - _color = color, - _packageResolver = packageResolver { + _color = color { _environment.defineFunction("variable-exists", r"$name", (arguments) { var variable = arguments[0].assertString("name"); return new SassBoolean(_environment.variableExists(variable.text)); @@ -207,12 +214,25 @@ class _EvaluateVisitor } EvaluateResult run(Stylesheet node) { - if (node.span?.sourceUrl != null) { - _activeImports.add(node.span.sourceUrl); - _importedFiles[node.span.sourceUrl] = node; + _baseUrl = node.span?.sourceUrl; + if (_baseUrl != null) { + if (_importer is FilesystemImporter) { + _includedFiles.add(p.fromUri(_baseUrl)); + } else { + _includedFiles.add(_baseUrl.toString()); + } + + var canonicalUrl = _importer?.canonicalize(_baseUrl); + if (canonicalUrl != null) { + _activeImports.add(canonicalUrl); + _importCache[canonicalUrl] = node; + } } + _baseUrl ??= new Uri(path: '.'); + visitStylesheet(node); - return new EvaluateResult(_root, new MapKeySet(_importedFiles)); + + return new EvaluateResult(_root, _includedFiles); } // ## Statements @@ -582,7 +602,9 @@ class _EvaluateVisitor /// Adds the stylesheet imported by [import] to the current document. void _visitDynamicImport(DynamicImport import) { - var stylesheet = _loadImport(import); + var result = _loadImport(import); + var importer = result.item1; + var stylesheet = result.item2; var url = stylesheet.span.sourceUrl; if (_activeImports.contains(url)) { @@ -592,89 +614,86 @@ class _EvaluateVisitor _activeImports.add(url); _withStackFrame("@import", import.span, () { _withEnvironment(_environment.global(), () { + var oldImporter = _importer; + var oldBaseUrl = _baseUrl; + _importer = importer; + _baseUrl = url; for (var statement in stylesheet.children) { statement.accept(this); } + _importer = oldImporter; + _baseUrl = oldBaseUrl; }); }); _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(_resolveImportUrl(import)); - var extension = p.extension(path); - var tryPath = extension == '.sass' || extension == '.scss' - ? _tryImportPath - : _tryImportPathWithExtensions; - - if (import.span.file.url != null) { - var base = p.dirname(p.fromUri(import.span.file.url)); - - var resolved = tryPath(p.join(base, path)); - if (resolved != null) return resolved; + Tuple2 _loadImport(DynamicImport import) { + try { + // 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 (stylesheet != null) return new Tuple2(_importer, stylesheet); } - for (var loadPath in _loadPaths) { - var resolved = tryPath(p.join(loadPath, path)); - if (resolved != null) return resolved; + for (var importer in _importers) { + var stylesheet = _tryImport(importer, import.url); + if (stylesheet != null) return new Tuple2(importer, stylesheet); } - }); - if (path == null) { - throw _exception("Can't find file to import.", import.span); - } - - var url = p.toUri(path); - return _importedFiles.putIfAbsent(url, () { - String contents; + if (import.url.scheme == 'package') { + // Special-case this error message, since it's tripped people up in the + // past. + throw "\"package:\" URLs aren't supported on this platform."; + } else { + throw "Can't find stylesheet to import."; + } + } on SassException catch (error) { + var frames = error.trace.frames.toList() + ..add(_stackFrame(import.span)) + ..addAll(_stack.toList()); + throw new SassRuntimeException( + error.message, error.span, new Trace(frames)); + } catch (error) { + String message; try { - contents = readFile(path); - } on SassException catch (error) { - var frames = _stack.toList()..add(_stackFrame(import.span)); - throw new SassRuntimeException( - error.message, error.span, new Trace(frames)); - } on FileSystemException catch (error) { - throw _exception(error.message, import.span); + message = error.message as String; + } catch (_) { + message = error.toString(); } - - return p.extension(path) == '.sass' - ? new Stylesheet.parseSass(contents, url: url, color: _color) - : new Stylesheet.parseScss(contents, url: url, color: _color); - }); + throw _exception(message, import.span); + } } - /// Like [_tryImportPath], but checks both `.sass` and `.scss` extensions. - String _tryImportPathWithExtensions(String path) => - _tryImportPath(path + '.sass') ?? _tryImportPath(path + '.scss'); + /// Parses the contents of [result] into a [Stylesheet]. + Stylesheet _tryImport(Importer importer, Uri url) { + // TODO(nweiz): Measure to see if it's worth caching this, too. + var canonicalUrl = importer.canonicalize(url); + if (canonicalUrl == null) return null; - /// If a file exists at [path], or a partial with the same name exists, - /// returns the resolved path. - /// - /// Otherwise, returns `null`. - String _tryImportPath(String path) { - var partial = p.join(p.dirname(path), "_${p.basename(path)}"); - if (fileExists(partial)) return partial; - if (fileExists(path)) return path; - return null; + return _importCache.putIfAbsent(canonicalUrl, () { + var result = importer.load(canonicalUrl); + if (result == null) return null; + + if (importer is FilesystemImporter) { + _includedFiles.add(p.fromUri(canonicalUrl)); + } else { + _includedFiles.add(url.toString()); + } + + // Use the canonicalized basename so that we display e.g. + // package:example/_example.scss rather than package:example/example in + // stack traces. + var displayUrl = url.resolve(p.basename(canonicalUrl.path)); + return result.isIndented + ? new Stylesheet.parseSass(result.contents, + url: displayUrl, color: _color) + : new Stylesheet.parseScss(result.contents, + url: displayUrl, color: _color); + }); } /// Adds a CSS import for [import]. @@ -1676,9 +1695,12 @@ class EvaluateResult { /// The CSS syntax tree. final CssStylesheet stylesheet; - /// The URLs that were loaded during the compilation, including the main - /// file's. - final Set includedUrls; + /// The set that will eventually populate the JS API's + /// `result.stats.includedFiles` field. + /// + /// For filesystem imports, this contains the import path. For all other + /// imports, it contains the URL passed to the `@import`. + final Set includedFiles; - EvaluateResult(this.stylesheet, this.includedUrls); + EvaluateResult(this.stylesheet, this.includedFiles); } diff --git a/pubspec.yaml b/pubspec.yaml index d53cb77a..88664a68 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: charcode: "^1.1.0" collection: "^1.8.0" convert: "^2.0.1" + meta: "^1.0.0" path: "^1.0.0" source_span: "^1.4.0" string_scanner: ">=0.1.5 <2.0.0"