Add support for the new importer API (#1522)

This commit is contained in:
Natalie Weizenbaum 2021-10-05 23:53:57 +00:00 committed by GitHub
parent 4b0f008760
commit c9e2f96f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 412 additions and 14 deletions

View File

@ -0,0 +1,56 @@
// Copyright 2021 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:async';
import 'package:node_interop/js.dart';
import 'package:node_interop/util.dart';
import '../../node/importer.dart';
import '../../node/url.dart';
import '../../node/utils.dart';
import '../../util/nullable.dart';
import '../async.dart';
import '../result.dart';
/// A wrapper for a potentially-asynchronous JS API importer that exposes it as
/// a Dart [AsyncImporter].
class NodeToDartAsyncImporter extends AsyncImporter {
/// The wrapped canonicalize function.
final Object? Function(String, CanonicalizeOptions) _canonicalize;
/// The wrapped load function.
final Object? Function(JSUrl) _load;
NodeToDartAsyncImporter(this._canonicalize, this._load);
FutureOr<Uri?> canonicalize(Uri url) async {
var result = _canonicalize(
url.toString(), CanonicalizeOptions(fromImport: fromImport));
if (isPromise(result)) result = await promiseToFuture(result as Promise);
if (result == null) return null;
if (isJSUrl(result)) return jsToDartUrl(result as JSUrl);
jsThrow(JsError("The canonicalize() method must return a URL."));
}
FutureOr<ImporterResult?> load(Uri url) async {
var result = _load(dartToJSUrl(url));
if (isPromise(result)) result = await promiseToFuture(result as Promise);
if (result == null) return null;
result as NodeImporterResult;
var contents = result.contents;
var syntax = result.syntax;
if (contents == null || syntax == null) {
jsThrow(JsError("The load() function must return an object with contents "
"and syntax fields."));
}
return ImporterResult(contents,
syntax: parseSyntax(syntax),
sourceMapUrl: result.sourceMapUrl.andThen(jsToDartUrl));
}
}

View File

@ -0,0 +1,80 @@
// Copyright 2021 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:async';
import 'package:node_interop/js.dart';
import 'package:node_interop/util.dart';
import 'package:path/path.dart' as p;
import '../../io.dart' as io;
import '../../node/importer.dart';
import '../../node/utils.dart';
import '../../syntax.dart';
import '../async.dart';
import '../filesystem.dart';
import '../result.dart';
import '../utils.dart';
/// A filesystem importer to use for most implementation details of
/// [NodeToDartAsyncFileImporter].
///
/// This allows us to avoid duplicating logic between the two importers.
final _filesystemImporter = FilesystemImporter('.');
/// A wrapper for a potentially-asynchronous JS API file importer that exposes
/// it as a Dart [AsyncImporter].
class NodeToDartAsyncFileImporter extends AsyncImporter {
/// The wrapped `findFileUrl` function.
final Object? Function(String, CanonicalizeOptions) _findFileUrl;
/// A map from canonical URLs to the `sourceMapUrl`s associated with them.
final _sourceMapUrls = <Uri, Uri>{};
NodeToDartAsyncFileImporter(this._findFileUrl);
FutureOr<Uri?> canonicalize(Uri url) async {
if (url.scheme != 'file' && url.scheme != '') return null;
var result = _findFileUrl(
url.toString(), CanonicalizeOptions(fromImport: fromImport));
if (isPromise(result)) result = await promiseToFuture(result as Promise);
if (result == null) return null;
result as NodeFileImporterResult;
var dartUrl = result.url;
var sourceMapUrl = result.sourceMapUrl;
if (dartUrl == null) {
jsThrow(JsError(
"The findFileUrl() method must return an object a url field."));
}
var resultUrl = jsToDartUrl(dartUrl);
if (resultUrl.scheme != 'file') {
jsThrow(JsError(
'The findFileUrl() must return a URL with scheme file://, was '
'"$url".'));
}
var canonical = _filesystemImporter.canonicalize(resultUrl);
if (canonical == null) return null;
if (sourceMapUrl != null) {
_sourceMapUrls[canonical] = jsToDartUrl(sourceMapUrl);
}
return canonical;
}
ImporterResult? load(Uri url) {
var path = p.fromUri(url);
return ImporterResult(io.readFile(path),
sourceMapUrl: _sourceMapUrls[url] ?? url, syntax: Syntax.forPath(path));
}
DateTime modificationTime(Uri url) =>
_filesystemImporter.modificationTime(url);
bool couldCanonicalize(Uri url, Uri canonicalUrl) =>
_filesystemImporter.couldCanonicalize(url, canonicalUrl);
}

View File

@ -0,0 +1,82 @@
// Copyright 2021 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:node_interop/js.dart';
import 'package:path/path.dart' as p;
import '../../io.dart' as io;
import '../../importer.dart';
import '../../node/importer.dart';
import '../../node/utils.dart';
import '../../syntax.dart';
import '../filesystem.dart';
import '../result.dart';
import '../utils.dart';
/// A filesystem importer to use for most implementation details of
/// [NodeToDartAsyncFileImporter].
///
/// This allows us to avoid duplicating logic between the two importers.
final _filesystemImporter = FilesystemImporter('.');
/// A wrapper for a potentially-asynchronous JS API file importer that exposes
/// it as a Dart [AsyncImporter].
class NodeToDartFileImporter extends Importer {
/// The wrapped `findFileUrl` function.
final Object? Function(String, CanonicalizeOptions) _findFileUrl;
/// A map from canonical URLs to the `sourceMapUrl`s associated with them.
final _sourceMapUrls = <Uri, Uri>{};
NodeToDartFileImporter(this._findFileUrl);
Uri? canonicalize(Uri url) {
if (url.scheme != 'file' && url.scheme != '') return null;
var result = _findFileUrl(
url.toString(), CanonicalizeOptions(fromImport: fromImport));
if (result == null) return null;
if (isPromise(result)) {
jsThrow(JsError(
"The canonicalize() function can't return a Promise for synchronous "
"compile functions."));
}
result as NodeFileImporterResult;
var dartUrl = result.url;
var sourceMapUrl = result.sourceMapUrl;
if (dartUrl == null) {
jsThrow(JsError(
"The findFileUrl() method must return an object a url field."));
}
var resultUrl = jsToDartUrl(dartUrl);
if (resultUrl.scheme != 'file') {
jsThrow(JsError(
'The findFileUrl() must return a URL with scheme file://, was '
'"$url".'));
}
var canonical = _filesystemImporter.canonicalize(resultUrl);
if (canonical == null) return null;
if (sourceMapUrl != null) {
_sourceMapUrls[canonical] = jsToDartUrl(sourceMapUrl);
}
return canonical;
}
ImporterResult? load(Uri url) {
var path = p.fromUri(url);
return ImporterResult(io.readFile(path),
sourceMapUrl: _sourceMapUrls[url] ?? url, syntax: Syntax.forPath(path));
}
DateTime modificationTime(Uri url) =>
_filesystemImporter.modificationTime(url);
bool couldCanonicalize(Uri url, Uri canonicalUrl) =>
_filesystemImporter.couldCanonicalize(url, canonicalUrl);
}

View File

@ -0,0 +1,62 @@
// Copyright 2021 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:node_interop/js.dart';
import '../../importer.dart';
import '../../node/importer.dart';
import '../../node/url.dart';
import '../../node/utils.dart';
import '../../util/nullable.dart';
import '../result.dart';
/// A wrapper for a synchronous JS API importer that exposes it as a Dart
/// [Importer].
class NodeToDartImporter extends Importer {
/// The wrapped canonicalize function.
final Object? Function(String, CanonicalizeOptions) _canonicalize;
/// The wrapped load function.
final Object? Function(JSUrl) _load;
NodeToDartImporter(this._canonicalize, this._load);
Uri? canonicalize(Uri url) {
var result = _canonicalize(
url.toString(), CanonicalizeOptions(fromImport: fromImport));
if (result == null) return null;
if (isJSUrl(result)) return jsToDartUrl(result as JSUrl);
if (isPromise(result)) {
jsThrow(JsError(
"The canonicalize() function can't return a Promise for synchronous "
"compile functions."));
} else {
jsThrow(JsError("The canonicalize() method must return a URL."));
}
}
ImporterResult? load(Uri url) {
var result = _load(dartToJSUrl(url));
if (result == null) return null;
if (isPromise(result)) {
jsThrow(JsError(
"The load() function can't return a Promise for synchronous compile "
"functions."));
}
result as NodeImporterResult;
var contents = result.contents;
var syntax = result.syntax;
if (contents == null || syntax == null) {
jsThrow(JsError("The load() function must return an object with contents "
"and syntax fields."));
}
return ImporterResult(contents,
syntax: parseSyntax(syntax),
sourceMapUrl: result.sourceMapUrl.andThen(jsToDartUrl));
}
}

View File

@ -8,6 +8,10 @@ import 'package:node_interop/util.dart';
import '../../sass.dart';
import '../importer/no_op.dart';
import '../importer/node_to_dart/async.dart';
import '../importer/node_to_dart/async_file.dart';
import '../importer/node_to_dart/file.dart';
import '../importer/node_to_dart/sync.dart';
import '../io.dart';
import '../logger.dart';
import '../logger/node_to_dart.dart';
@ -15,6 +19,7 @@ import '../util/nullable.dart';
import 'compile_options.dart';
import 'compile_result.dart';
import 'exception.dart';
import 'importer.dart';
import 'utils.dart';
/// The JS API `compile` function.
@ -31,7 +36,8 @@ NodeCompileResult compile(String path, [CompileOptions? options]) {
style: _parseOutputStyle(options?.style),
verbose: options?.verbose ?? false,
sourceMap: options?.sourceMap ?? false,
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)));
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)),
importers: options?.importers?.map(_parseImporter));
return _convertResult(result);
} on SassException catch (error) {
throwNodeException(error, color: color);
@ -46,16 +52,18 @@ NodeCompileResult compileString(String text, [CompileStringOptions? options]) {
var color = options?.alertColor ?? hasTerminal;
try {
var result = compileStringToResult(text,
syntax: _parseSyntax(options?.syntax),
syntax: parseSyntax(options?.syntax),
url: options?.url.andThen(jsToDartUrl),
importer: options?.url == null ? NoOpImporter() : null,
color: color,
loadPaths: options?.loadPaths,
quietDeps: options?.quietDeps ?? false,
style: _parseOutputStyle(options?.style),
verbose: options?.verbose ?? false,
sourceMap: options?.sourceMap ?? false,
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)));
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)),
importers: options?.importers?.map(_parseImporter),
importer: options?.importer.andThen(_parseImporter) ??
(options?.url == null ? NoOpImporter() : null));
return _convertResult(result);
} on SassException catch (error) {
throwNodeException(error, color: color);
@ -76,7 +84,9 @@ Promise compileAsync(String path, [CompileOptions? options]) {
style: _parseOutputStyle(options?.style),
verbose: options?.verbose ?? false,
sourceMap: options?.sourceMap ?? false,
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)));
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)),
importers: options?.importers
?.map((importer) => _parseAsyncImporter(importer)));
return _convertResult(result);
}()), color: color);
}
@ -89,16 +99,20 @@ Promise compileStringAsync(String text, [CompileStringOptions? options]) {
var color = options?.alertColor ?? hasTerminal;
return _wrapAsyncSassExceptions(futureToPromise(() async {
var result = await compileStringToResultAsync(text,
syntax: _parseSyntax(options?.syntax),
syntax: parseSyntax(options?.syntax),
url: options?.url.andThen(jsToDartUrl),
importer: options?.url == null ? NoOpImporter() : null,
color: color,
loadPaths: options?.loadPaths,
quietDeps: options?.quietDeps ?? false,
style: _parseOutputStyle(options?.style),
verbose: options?.verbose ?? false,
sourceMap: options?.sourceMap ?? false,
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)));
logger: NodeToDartLogger(options?.logger, Logger.stderr(color: color)),
importers: options?.importers
?.map((importer) => _parseAsyncImporter(importer)),
importer: options?.importer
.andThen((importer) => _parseAsyncImporter(importer)) ??
(options?.url == null ? NoOpImporter() : null));
return _convertResult(result);
}()), color: color);
}
@ -136,10 +150,49 @@ OutputStyle _parseOutputStyle(String? style) {
jsThrow(JsError('Unknown output style "$style".'));
}
/// Converts a syntax string to an instance of [Syntax].
Syntax _parseSyntax(String? syntax) {
if (syntax == null || syntax == 'scss') return Syntax.scss;
if (syntax == 'indented') return Syntax.sass;
if (syntax == 'css') return Syntax.css;
jsThrow(JsError('Unknown syntax "$syntax".'));
/// Converts [importer] into an [AsyncImporter] that can be used with
/// [compileAsync] or [compileStringAsync].
AsyncImporter _parseAsyncImporter(Object? importer) {
if (importer == null) jsThrow(JsError("Importers may not be null."));
importer as NodeImporter;
var findFileUrl = importer.findFileUrl;
var canonicalize = importer.canonicalize;
var load = importer.load;
if (findFileUrl == null) {
if (canonicalize == null || load == null) {
jsThrow(JsError(
"An importer must have either canonicalize and load methods, or a "
"findFileUrl method."));
}
return NodeToDartAsyncImporter(canonicalize, load);
} else if (canonicalize != null || load != null) {
jsThrow(JsError("An importer may not have a findFileUrl method as well as "
"canonicalize and load methods."));
} else {
return NodeToDartAsyncFileImporter(findFileUrl);
}
}
/// Converts [importer] into a synchronous [Importer].
Importer _parseImporter(Object? importer) {
if (importer == null) jsThrow(JsError("Importers may not be null."));
importer as NodeImporter;
var findFileUrl = importer.findFileUrl;
var canonicalize = importer.canonicalize;
var load = importer.load;
if (findFileUrl == null) {
if (canonicalize == null || load == null) {
jsThrow(JsError(
"An importer must have either canonicalize and load methods, or a "
"findFileUrl method."));
}
return NodeToDartImporter(canonicalize, load);
} else if (canonicalize != null || load != null) {
jsThrow(JsError("An importer may not have a findFileUrl method as well as "
"canonicalize and load methods."));
} else {
return NodeToDartFileImporter(findFileUrl);
}
}

View File

@ -4,6 +4,7 @@
import 'package:js/js.dart';
import 'importer.dart';
import 'logger.dart';
import 'url.dart';
@ -18,6 +19,7 @@ class CompileOptions {
external bool? get verbose;
external bool? get sourceMap;
external NodeLogger? get logger;
external List<Object?>? get importers;
}
@JS()
@ -25,4 +27,5 @@ class CompileOptions {
class CompileStringOptions extends CompileOptions {
external String? get syntax;
external JSUrl? get url;
external NodeImporter? get importer;
}

View File

@ -0,0 +1,39 @@
// Copyright 2021 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:js/js.dart';
import 'url.dart';
@JS()
@anonymous
class NodeImporter {
external Object? Function(String, CanonicalizeOptions)? get canonicalize;
external Object? Function(JSUrl)? get load;
external NodeFileImporterResult? Function(String, CanonicalizeOptions)?
get findFileUrl;
}
@JS()
@anonymous
class CanonicalizeOptions {
external bool get fromImport;
external factory CanonicalizeOptions({bool fromImport});
}
@JS()
@anonymous
class NodeImporterResult {
external String? get contents;
external String? get syntax;
external JSUrl? get sourceMapUrl;
}
@JS()
@anonymous
class NodeFileImporterResult {
external JSUrl? get url;
external JSUrl? get sourceMapUrl;
}

View File

@ -6,9 +6,11 @@ import 'dart:js';
import 'dart:js_util';
import 'dart:typed_data';
import 'package:node_interop/js.dart';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import '../syntax.dart';
import 'array.dart';
import 'function.dart';
import 'url.dart';
@ -160,6 +162,19 @@ void _addGettersToPrototype(Object prototype, Map<String, Function> getters) {
/// Returns whether [value] is truthy according to JavaScript.
bool isTruthy(Object? value) => value != false && value != null;
@JS('Promise')
external Function get _promiseClass;
/// Returns whether [object] is a `Promise`.
bool isPromise(Object? object) =>
object != null && instanceof(object, _promiseClass);
@JS('URL')
external Function get _urlClass;
/// Returns whether [object] is a JavaScript `URL`.
bool isJSUrl(Object? object) => object != null && instanceof(object, _urlClass);
@JS('Buffer.from')
external Uint8List _buffer(String text, String encoding);
@ -188,3 +203,11 @@ JSArray toJSArray(Iterable<Object?> iterable) {
}
return array;
}
/// Converts a syntax string to an instance of [Syntax].
Syntax parseSyntax(String? syntax) {
if (syntax == null || syntax == 'scss') return Syntax.scss;
if (syntax == 'indented') return Syntax.sass;
if (syntax == 'css') return Syntax.css;
jsThrow(JsError('Unknown syntax "$syntax".'));
}