From 8d7f29fe04a95c7e2e485c0a7302a5a545a79812 Mon Sep 17 00:00:00 2001 From: "rnystrom@google.com" <rnystrom@google.com> Date: Wed, 26 Feb 2014 21:14:14 +0000 Subject: [PATCH] Allow transformers to exclude and include assets. BUG=https://code.google.com/p/dart/issues/detail?id=14594 R=nweiz@google.com Review URL: https://codereview.chromium.org//169223010 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@33089 260f80e4-7a28-3924-810f-c04153c831b5 --- lib/src/barback.dart | 77 ++++++++++++++++++- lib/src/barback/excluding_transformer.dart | 58 ++++++++++++++ lib/src/barback/load_all_transformers.dart | 5 ++ lib/src/barback/load_transformers.dart | 20 +++-- lib/src/pubspec.dart | 11 +-- test/pubspec_test.dart | 53 +++++++++++-- .../exclusion/exclude_asset_list_test.dart | 48 ++++++++++++ .../exclusion/exclude_asset_string_test.dart | 45 +++++++++++ .../exclusion/include_asset_list_test.dart | 48 ++++++++++++ .../exclusion/include_asset_string_test.dart | 45 +++++++++++ .../includes_before_excludes_test.dart | 44 +++++++++++ .../exclusion/works_on_dart2js_test.dart | 43 +++++++++++ .../works_on_transformer_group_test.dart | 59 ++++++++++++++ ...ubspec_with_reserved_transformer_test.dart | 2 +- 14 files changed, 534 insertions(+), 24 deletions(-) create mode 100644 lib/src/barback/excluding_transformer.dart create mode 100644 test/transformer/exclusion/exclude_asset_list_test.dart create mode 100644 test/transformer/exclusion/exclude_asset_string_test.dart create mode 100644 test/transformer/exclusion/include_asset_list_test.dart create mode 100644 test/transformer/exclusion/include_asset_string_test.dart create mode 100644 test/transformer/exclusion/includes_before_excludes_test.dart create mode 100644 test/transformer/exclusion/works_on_dart2js_test.dart create mode 100644 test/transformer/exclusion/works_on_transformer_group_test.dart diff --git a/lib/src/barback.dart b/lib/src/barback.dart index ffd301f8..b0a5581a 100644 --- a/lib/src/barback.dart +++ b/lib/src/barback.dart @@ -60,6 +60,27 @@ class TransformerId { /// This will be an empty map if no configuration was provided. final Map configuration; + /// The primary input inclusions. + /// + /// Each inclusion is an asset path. If this set is non-empty, than *only* + /// matching assets are allowed as a primary input by this transformer. If + /// `null`, all assets are included. + /// + /// This is processed before [excludes]. If a transformer has both includes + /// and excludes, then the set of included assets is determined and assets + /// are excluded from that resulting set. + final Set<String> includes; + + /// The primary input exclusions. + /// + /// Any asset whose pach is in this is not allowed as a primary input by + /// this transformer. + /// + /// This is processed after [includes]. If a transformer has both includes + /// and excludes, then the set of included assets is determined and assets + /// are excluded from that resulting set. + final Set<String> excludes; + /// Whether this ID points to a built-in transformer exposed by pub. bool get isBuiltInTransformer => package.startsWith('\$'); @@ -79,11 +100,63 @@ class TransformerId { if (parts.length == 1) { return new TransformerId(parts.single, null, configuration); } + return new TransformerId(parts.first, parts.last, configuration); } - TransformerId(this.package, this.path, Map configuration) - : configuration = configuration == null ? {} : configuration { + factory TransformerId(String package, String path, Map configuration) { + parseField(key) { + if (!configuration.containsKey(key)) return null; + var field = configuration.remove(key); + + if (field is String) return new Set<String>.from([field]); + + if (field is List) { + var nonstrings = field + .where((element) => element is! String) + .map((element) => '"$element"'); + + if (nonstrings.isNotEmpty) { + throw new FormatException( + '"$key" list field may only contain strings, but contained ' + '${toSentence(nonstrings)}.'); + } + + return new Set<String>.from(field); + } else { + throw new FormatException( + '"$key" field must be a string or list, but was "$field".'); + } + } + + var includes = null; + var excludes = null; + + if (configuration == null) { + configuration = {}; + } else { + // Pull out the exclusions/inclusions. + includes = parseField("\$include"); + excludes = parseField("\$exclude"); + + // All other keys starting with "$" are unexpected. + var reservedKeys = configuration.keys + .where((key) => key is String && key.startsWith(r'$')) + .map((key) => '"$key"'); + + if (reservedKeys.isNotEmpty) { + throw new FormatException( + 'Unknown reserved ${pluralize('field', reservedKeys.length)} ' + '${toSentence(reservedKeys)}.'); + } + } + + return new TransformerId._(package, path, configuration, + includes, excludes); + } + + TransformerId._(this.package, this.path, this.configuration, + this.includes, this.excludes) { if (!package.startsWith('\$')) return; if (_BUILT_IN_TRANSFORMERS.contains(package)) return; throw new FormatException('Unsupported built-in transformer $package.'); diff --git a/lib/src/barback/excluding_transformer.dart b/lib/src/barback/excluding_transformer.dart new file mode 100644 index 00000000..03b485b6 --- /dev/null +++ b/lib/src/barback/excluding_transformer.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2014, 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. + +library pub.excluding_transformer; + +import 'dart:async'; + +import 'package:barback/barback.dart'; + +/// Decorates an inner [Transformer] and handles including and excluding +/// primary inputs. +class ExcludingTransformer extends Transformer { + /// If [includes] or [excludes] is non-null, wraps [inner] in an + /// [ExcludingTransformer] that handles those. + /// + /// Otherwise, just returns [inner] unmodified. + static Transformer wrap(Transformer inner, Set<String> includes, + Set<String> excludes) { + if (includes == null && excludes == null) return inner; + + return new ExcludingTransformer._(inner, includes, excludes); + } + + final Transformer _inner; + + /// The set of asset paths which should be included. + /// + /// If `null`, all non-excluded assets are allowed. Otherwise, only included + /// assets are allowed. + final Set<String> _includes; + + /// The set of assets which should be excluded. + /// + /// Exclusions are applied after inclusions. + final Set<String> _excludes; + + ExcludingTransformer._(this._inner, this._includes, this._excludes); + + Future<bool> isPrimary(Asset asset) { + // TODO(rnystrom): Support globs in addition to paths. See #17093. + if (_includes != null) { + // If there are any includes, it must match one of them. + if (!_includes.contains(asset.id.path)) return new Future.value(false); + } + + // It must not be excluded. + if (_excludes != null && _excludes.contains(asset.id.path)) { + return new Future.value(false); + } + + return _inner.isPrimary(asset); + } + + Future apply(Transform transform) => _inner.apply(transform); + + String toString() => _inner.toString(); +} diff --git a/lib/src/barback/load_all_transformers.dart b/lib/src/barback/load_all_transformers.dart index 7ff0dc56..21585cd9 100644 --- a/lib/src/barback/load_all_transformers.dart +++ b/lib/src/barback/load_all_transformers.dart @@ -10,6 +10,7 @@ import 'package:barback/barback.dart'; import 'build_environment.dart'; import 'dart2js_transformer.dart'; +import 'excluding_transformer.dart'; import 'load_transformers.dart'; import 'rewrite_import_transformer.dart'; import '../barback.dart'; @@ -275,6 +276,10 @@ class _TransformerLoader { try { transformer = new Dart2JSTransformer.withSettings(_environment, new BarbackSettings(id.configuration, _environment.mode)); + + // Handle any exclusions. + transformer = ExcludingTransformer.wrap(transformer, + id.includes, id.excludes); } on FormatException catch (error, stackTrace) { fail(error.message, error, stackTrace); } diff --git a/lib/src/barback/load_transformers.dart b/lib/src/barback/load_transformers.dart index 72768f47..12e5c499 100644 --- a/lib/src/barback/load_transformers.dart +++ b/lib/src/barback/load_transformers.dart @@ -19,6 +19,7 @@ import '../dart.dart' as dart; import '../log.dart' as log; import '../utils.dart'; import 'build_environment.dart'; +import 'excluding_transformer.dart'; /// A Dart script to run in an isolate. /// @@ -386,7 +387,9 @@ Future<Set> loadTransformers(BuildEnvironment environment, TransformerId id) { // TODO(nweiz): support non-JSON-encodable configuration maps. 'configuration': JSON.encode(id.configuration) }).then((transformers) { - transformers = transformers.map(_deserializeTransformerOrGroup).toSet(); + transformers = transformers.map( + (transformer) => _deserializeTransformerOrGroup(transformer, id)) + .toSet(); log.fine("Transformers from $assetId: $transformers"); return transformers; }); @@ -447,9 +450,10 @@ class _ForeignGroup implements TransformerGroup { /// The result of calling [toString] on the transformer group in the isolate. final String _toString; - _ForeignGroup(Map map) + _ForeignGroup(TransformerId id, Map map) : phases = map['phases'].map((phase) { - return phase.map(_deserializeTransformerOrGroup).toList(); + return phase.map((transformer) => _deserializeTransformerOrGroup( + transformer, id)).toList(); }).toList(), _toString = map['toString']; @@ -457,10 +461,14 @@ class _ForeignGroup implements TransformerGroup { } /// Converts a serializable map into a [Transformer] or a [TransformerGroup]. -_deserializeTransformerOrGroup(Map map) { - if (map['type'] == 'Transformer') return new _ForeignTransformer(map); +_deserializeTransformerOrGroup(Map map, TransformerId id) { + if (map['type'] == 'Transformer') { + var transformer = new _ForeignTransformer(map); + return ExcludingTransformer.wrap(transformer, id.includes, id.excludes); + } + assert(map['type'] == 'TransformerGroup'); - return new _ForeignGroup(map); + return new _ForeignGroup(id, map); } /// Converts [transform] into a serializable map. diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart index b44bb555..27480617 100644 --- a/lib/src/pubspec.dart +++ b/lib/src/pubspec.dart @@ -162,18 +162,9 @@ class Pubspec { _error('"$field.$library" field must be a map, but was ' '"$configuration".'); } - - var reservedKeys = configuration.keys - .where((key) => key is String && key.startsWith(r'$')) - .map((key) => '"$key"'); - if (reservedKeys.isNotEmpty) { - _error('"$field.$library" field cannot contain reserved ' - '${pluralize('field', reservedKeys.length)} ' - '${toSentence(reservedKeys)}.'); - } } - var id = _wrapFormatException("transformer identifier", + var id = _wrapFormatException("transformer configuration", "$field.$library", () => new TransformerId.parse(library, configuration)); diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart index f451f1fb..a730a14b 100644 --- a/test/pubspec_test.dart +++ b/test/pubspec_test.dart @@ -241,13 +241,14 @@ dependencies: '"transformers.pkg" field must be a map'); }); - test("throws if a transformer's configuration contains a top-level key " - "beginning with a dollar sign", () { + test("throws if a transformer's configuration contains an unknown " + "reserved key at the top level", () { expectPubspecException(''' name: pkg transformers: [{pkg: {\$key: "value"}}]''', (pubspec) => pubspec.transformers, - '"transformers.pkg" field cannot contain reserved field "\$key"'); + 'Invalid transformer configuration for "transformers.pkg": ' + 'Unknown reserved field "\$key"'); }); test("doesn't throw if a transformer's configuration contains a " @@ -262,13 +263,55 @@ transformers: expect(pkg.configuration["outer"]["\$inner"], equals("value")); }); + test("throws if the \$include value is not a string or list", () { + expectPubspecException(''' +name: pkg +transformers: +- pkg: {\$include: 123}''', + (pubspec) => pubspec.transformers, + 'Invalid transformer configuration for "transformers.pkg": ' + '"\$include" field must be a string or list, but was "123"'); + }); + + test("throws if the \$include list contains a non-string", () { + expectPubspecException(''' +name: pkg +transformers: +- pkg: {\$include: ["ok", 123, "alright", null]}''', + (pubspec) => pubspec.transformers, + 'Invalid transformer configuration for "transformers.pkg": ' + '"\$include" list field may only contain strings, but contained ' + '"123" and "null"'); + }); + + test("throws if the \$exclude value is not a string or list", () { + expectPubspecException(''' +name: pkg +transformers: +- pkg: {\$exclude: 123}''', + (pubspec) => pubspec.transformers, + 'Invalid transformer configuration for "transformers.pkg": ' + '"\$exclude" field must be a string or list, but was "123"'); + }); + + test("throws if the \$exclude list contains a non-string", () { + expectPubspecException(''' +name: pkg +transformers: +- pkg: {\$exclude: ["ok", 123, "alright", null]}''', + (pubspec) => pubspec.transformers, + 'Invalid transformer configuration for "transformers.pkg": ' + '"\$exclude" list field may only contain strings, but contained ' + '"123" and "null"'); + }); + test("throws if a transformer is not from a dependency", () { expectPubspecException(''' name: pkg transformers: [foo] ''', - (pubspec) => pubspec.transformers, - '"transformers.foo" refers to a package that\'s not a dependency.'); + (pubspec) => pubspec.transformers, + '"transformers.foo" refers to a package that\'s not a dependency.'); }); test("allows a transformer from a normal dependency", () { diff --git a/test/transformer/exclusion/exclude_asset_list_test.dart b/test/transformer/exclusion/exclude_asset_list_test.dart new file mode 100644 index 00000000..81fc9ca0 --- /dev/null +++ b/test/transformer/exclusion/exclude_asset_list_test.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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. + +library pub_tests; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../../serve/utils.dart'; + +main() { + initConfig(); + integration("excludes a list of assets", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + { + "myapp/src/transformer": { + "\$exclude": [ + "web/foo.txt", + "web/sub/foo.txt" + ] + } + } + ] + }), + d.dir("lib", [d.dir("src", [ + d.file("transformer.dart", REWRITE_TRANSFORMER) + ])]), + d.dir("web", [ + d.file("foo.txt", "foo"), + d.file("bar.txt", "bar"), + d.dir("sub", [ + d.file("foo.txt", "foo"), + ]) + ]) + ]).create(); + + createLockFile('myapp', pkg: ['barback']); + + pubServe(); + requestShould404("foo.out"); + requestShould404("sub/foo.out"); + requestShouldSucceed("bar.out", "bar.out"); + endPubServe(); + }); +} diff --git a/test/transformer/exclusion/exclude_asset_string_test.dart b/test/transformer/exclusion/exclude_asset_string_test.dart new file mode 100644 index 00000000..b5bf7141 --- /dev/null +++ b/test/transformer/exclusion/exclude_asset_string_test.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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. + +library pub_tests; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../../serve/utils.dart'; + +main() { + initConfig(); + integration("allows a single string as the asset to exclude", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + { + "myapp/src/transformer": { + "\$exclude": "web/foo.txt" + } + } + ] + }), + d.dir("lib", [d.dir("src", [ + d.file("transformer.dart", REWRITE_TRANSFORMER) + ])]), + d.dir("web", [ + d.file("foo.txt", "foo"), + d.file("bar.txt", "bar"), + d.dir("sub", [ + d.file("foo.txt", "foo"), + ]) + ]) + ]).create(); + + createLockFile('myapp', pkg: ['barback']); + + pubServe(); + requestShould404("foo.out"); + requestShouldSucceed("sub/foo.out", "foo.out"); + requestShouldSucceed("bar.out", "bar.out"); + endPubServe(); + }); +} diff --git a/test/transformer/exclusion/include_asset_list_test.dart b/test/transformer/exclusion/include_asset_list_test.dart new file mode 100644 index 00000000..aac64323 --- /dev/null +++ b/test/transformer/exclusion/include_asset_list_test.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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. + +library pub_tests; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../../serve/utils.dart'; + +main() { + initConfig(); + integration("includes assets", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + { + "myapp/src/transformer": { + "\$include": [ + "web/foo.txt", + "web/sub/foo.txt" + ] + } + } + ] + }), + d.dir("lib", [d.dir("src", [ + d.file("transformer.dart", REWRITE_TRANSFORMER) + ])]), + d.dir("web", [ + d.file("foo.txt", "foo"), + d.file("bar.txt", "bar"), + d.dir("sub", [ + d.file("foo.txt", "foo"), + ]) + ]) + ]).create(); + + createLockFile('myapp', pkg: ['barback']); + + pubServe(); + requestShouldSucceed("foo.out", "foo.out"); + requestShouldSucceed("sub/foo.out", "foo.out"); + requestShould404("bar.out"); + endPubServe(); + }); +} diff --git a/test/transformer/exclusion/include_asset_string_test.dart b/test/transformer/exclusion/include_asset_string_test.dart new file mode 100644 index 00000000..02891267 --- /dev/null +++ b/test/transformer/exclusion/include_asset_string_test.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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. + +library pub_tests; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../../serve/utils.dart'; + +main() { + initConfig(); + integration("allows a single string as the asset to include", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + { + "myapp/src/transformer": { + "\$include": "web/foo.txt" + } + } + ] + }), + d.dir("lib", [d.dir("src", [ + d.file("transformer.dart", REWRITE_TRANSFORMER) + ])]), + d.dir("web", [ + d.file("foo.txt", "foo"), + d.file("bar.txt", "bar"), + d.dir("sub", [ + d.file("foo.txt", "foo"), + ]) + ]) + ]).create(); + + createLockFile('myapp', pkg: ['barback']); + + pubServe(); + requestShouldSucceed("foo.out", "foo.out"); + requestShould404("sub/foo.out"); + requestShould404("bar.out"); + endPubServe(); + }); +} diff --git a/test/transformer/exclusion/includes_before_excludes_test.dart b/test/transformer/exclusion/includes_before_excludes_test.dart new file mode 100644 index 00000000..4d7f436d --- /dev/null +++ b/test/transformer/exclusion/includes_before_excludes_test.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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. + +library pub_tests; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../../serve/utils.dart'; + +main() { + initConfig(); + integration("applies includes before excludes if both are present", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + { + "myapp/src/transformer": { + "\$include": ["web/a.txt", "web/b.txt"], + "\$exclude": "web/a.txt" + } + } + ] + }), + d.dir("lib", [d.dir("src", [ + d.file("transformer.dart", REWRITE_TRANSFORMER) + ])]), + d.dir("web", [ + d.file("a.txt", "a.txt"), + d.file("b.txt", "b.txt"), + d.file("c.txt", "c.txt") + ]) + ]).create(); + + createLockFile('myapp', pkg: ['barback']); + + pubServe(); + requestShould404("a.out"); + requestShouldSucceed("b.out", "b.txt.out"); + requestShould404("c.out"); + endPubServe(); + }); +} diff --git a/test/transformer/exclusion/works_on_dart2js_test.dart b/test/transformer/exclusion/works_on_dart2js_test.dart new file mode 100644 index 00000000..aedfbf97 --- /dev/null +++ b/test/transformer/exclusion/works_on_dart2js_test.dart @@ -0,0 +1,43 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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. + +library pub_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../../serve/utils.dart'; + +main() { + initConfig(); + integration("works on the dart2js transformer", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + { + "\$dart2js": { + "\$include": ["web/a.dart", "web/b.dart"], + "\$exclude": "web/a.dart" + } + } + ] + }), + d.dir("web", [ + d.file("a.dart", "void main() => print('hello');"), + d.file("b.dart", "void main() => print('hello');"), + d.file("c.dart", "void main() => print('hello');") + ]) + ]).create(); + + createLockFile('myapp', pkg: ['barback']); + + pubServe(); + requestShould404("a.dart.js"); + requestShouldSucceed("b.dart.js", isNot(isEmpty)); + requestShould404("c.dart.js"); + endPubServe(); + }); +} diff --git a/test/transformer/exclusion/works_on_transformer_group_test.dart b/test/transformer/exclusion/works_on_transformer_group_test.dart new file mode 100644 index 00000000..d4164bd1 --- /dev/null +++ b/test/transformer/exclusion/works_on_transformer_group_test.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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. + +library pub_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../../serve/utils.dart'; + +const GROUP = """ +import 'package:barback/barback.dart'; + +import 'transformer.dart'; + +class RewriteGroup implements TransformerGroup { + RewriteGroup.asPlugin(); + + Iterable<Iterable> get phases => [[new RewriteTransformer.asPlugin()]]; +} +"""; + +main() { + initConfig(); + integration("works on a transformer group", () { + d.dir(appPath, [ + d.pubspec({ + "name": "myapp", + "transformers": [ + { + "myapp/src/group": { + "\$include": ["web/a.txt", "web/b.txt"], + "\$exclude": "web/a.txt" + } + } + ] + }), + d.dir("lib", [d.dir("src", [ + d.file("transformer.dart", REWRITE_TRANSFORMER), + d.file("group.dart", GROUP) + ])]), + d.dir("web", [ + d.file("a.txt", "a.txt"), + d.file("b.txt", "b.txt"), + d.file("c.txt", "c.txt") + ]) + ]).create(); + + createLockFile('myapp', pkg: ['barback']); + + pubServe(); + requestShould404("a.out"); + requestShouldSucceed("b.out", "b.txt.out"); + requestShould404("c.out"); + endPubServe(); + }); +} diff --git a/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart b/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart index f004b236..0b00b037 100644 --- a/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart +++ b/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart @@ -29,7 +29,7 @@ main() { var pub = startPubServe(); pub.stderr.expect(emitsLines( 'Error in pubspec for package "myapp" loaded from pubspec.yaml:\n' - 'Invalid transformer identifier for "transformers.\$nonexistent": ' + 'Invalid transformer configuration for "transformers.\$nonexistent": ' 'Unsupported built-in transformer \$nonexistent.')); pub.shouldExit(1); }); -- GitLab