From c98cfd53b08640db7cfe7b341dfdaefd082b85a5 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 11 Mar 2019 16:49:58 -0700 Subject: [PATCH] Emit CSS for used modules (#620) --- lib/src/async_environment.dart | 27 ++++++++++++++--- lib/src/async_module.dart | 3 ++ lib/src/environment.dart | 29 +++++++++++++++---- lib/src/module.dart | 5 +++- lib/src/visitor/async_evaluate.dart | 43 ++++++++++++++++++++++++++- lib/src/visitor/evaluate.dart | 45 +++++++++++++++++++++++++++-- 6 files changed, 139 insertions(+), 13 deletions(-) diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index 71eb5758..0433829f 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -29,6 +29,10 @@ class AsyncEnvironment { /// This is `null` if there are no namespaceless modules. Set _globalModules; + /// Modules from both [_modules] and [_global], in the order in which they + /// were `@use`d. + final List _allModules; + /// A list of variables defined at each lexical scope level. /// /// Each scope maps the names of declared variables to their values. These @@ -122,6 +126,7 @@ class AsyncEnvironment { AsyncEnvironment({bool sourceMap = false}) : _modules = {}, _globalModules = null, + _allModules = [], _variables = [normalizedMap()], _variableNodes = sourceMap ? [normalizedMap()] : null, _variableIndices = normalizedMap(), @@ -132,8 +137,15 @@ class AsyncEnvironment { coreFunctions.forEach(setFunction); } - AsyncEnvironment._(this._modules, this._globalModules, this._variables, - this._variableNodes, this._functions, this._mixins, this._content) + AsyncEnvironment._( + this._modules, + this._globalModules, + this._allModules, + this._variables, + this._variableNodes, + this._functions, + this._mixins, + this._content) // Lazily fill in the indices rather than eagerly copying them from the // existing environment in closure() because the copying took a lot of // time and was rarely helpful. This saves a bunch of time on Susy's @@ -150,6 +162,7 @@ class AsyncEnvironment { AsyncEnvironment closure() => AsyncEnvironment._( _modules, _globalModules, + _allModules, _variables.toList(), _variableNodes?.toList(), _functions.toList(), @@ -160,8 +173,10 @@ class AsyncEnvironment { /// /// The returned environment shares this environment's global variables, /// functions, and mixins, but not its modules. - AsyncEnvironment global() => AsyncEnvironment._({}, + AsyncEnvironment global() => AsyncEnvironment._( + {}, null, + [], _variables.toList(), _variableNodes?.toList(), _functions.toList(), @@ -180,6 +195,7 @@ class AsyncEnvironment { if (namespace == null) { _globalModules ??= Set(); _globalModules.add(module); + _allModules.add(module); for (var name in _variables.first.keys) { if (module.variables.containsKey(name)) { @@ -195,6 +211,7 @@ class AsyncEnvironment { } _modules[namespace] = module; + _allModules.add(module); } } @@ -600,6 +617,7 @@ class AsyncEnvironment { /// A module that represents the top-level members defined in an [Environment]. class _EnvironmentModule implements AsyncModule { + final List upstream; final Map variables; final Map variableNodes; final Map functions; @@ -612,7 +630,8 @@ class _EnvironmentModule implements AsyncModule { // TODO(nweiz): Use custom [UnmodifiableMapView]s that forbid access to // private members. _EnvironmentModule(this._environment, this.css) - : variables = PublicMemberMap(_environment._variables.first), + : upstream = _environment._allModules, + variables = PublicMemberMap(_environment._variables.first), variableNodes = _environment._variableNodes == null ? null : PublicMemberMap(_environment._variableNodes.first), diff --git a/lib/src/async_module.dart b/lib/src/async_module.dart index 1aad964d..267eb1dc 100644 --- a/lib/src/async_module.dart +++ b/lib/src/async_module.dart @@ -11,6 +11,9 @@ import 'value.dart'; /// The interface for a Sass module. abstract class AsyncModule { + /// Modules that this module uses. + List get upstream; + /// The module's variables. Map get variables; diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 012adc1b..56772493 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/synchronize.dart for details. // -// Checksum: 77b03257b1770270e2d1f269281bdaab3f9f1ae9 +// Checksum: e1d3693a4ede6e31d259efb13148a669be01e98c // // ignore_for_file: unused_import @@ -34,6 +34,10 @@ class Environment { /// This is `null` if there are no namespaceless modules. Set _globalModules; + /// Modules from both [_modules] and [_global], in the order in which they + /// were `@use`d. + final List _allModules; + /// A list of variables defined at each lexical scope level. /// /// Each scope maps the names of declared variables to their values. These @@ -127,6 +131,7 @@ class Environment { Environment({bool sourceMap = false}) : _modules = {}, _globalModules = null, + _allModules = [], _variables = [normalizedMap()], _variableNodes = sourceMap ? [normalizedMap()] : null, _variableIndices = normalizedMap(), @@ -137,8 +142,15 @@ class Environment { coreFunctions.forEach(setFunction); } - Environment._(this._modules, this._globalModules, this._variables, - this._variableNodes, this._functions, this._mixins, this._content) + Environment._( + this._modules, + this._globalModules, + this._allModules, + this._variables, + this._variableNodes, + this._functions, + this._mixins, + this._content) // Lazily fill in the indices rather than eagerly copying them from the // existing environment in closure() because the copying took a lot of // time and was rarely helpful. This saves a bunch of time on Susy's @@ -155,6 +167,7 @@ class Environment { Environment closure() => Environment._( _modules, _globalModules, + _allModules, _variables.toList(), _variableNodes?.toList(), _functions.toList(), @@ -165,8 +178,10 @@ class Environment { /// /// The returned environment shares this environment's global variables, /// functions, and mixins, but not its modules. - Environment global() => Environment._({}, + Environment global() => Environment._( + {}, null, + [], _variables.toList(), _variableNodes?.toList(), _functions.toList(), @@ -185,6 +200,7 @@ class Environment { if (namespace == null) { _globalModules ??= Set(); _globalModules.add(module); + _allModules.add(module); for (var name in _variables.first.keys) { if (module.variables.containsKey(name)) { @@ -200,6 +216,7 @@ class Environment { } _modules[namespace] = module; + _allModules.add(module); } } @@ -602,6 +619,7 @@ class Environment { /// A module that represents the top-level members defined in an [Environment]. class _EnvironmentModule implements Module { + final List upstream; final Map variables; final Map variableNodes; final Map functions; @@ -614,7 +632,8 @@ class _EnvironmentModule implements Module { // TODO(nweiz): Use custom [UnmodifiableMapView]s that forbid access to // private members. _EnvironmentModule(this._environment, this.css) - : variables = PublicMemberMap(_environment._variables.first), + : upstream = _environment._allModules, + variables = PublicMemberMap(_environment._variables.first), variableNodes = _environment._variableNodes == null ? null : PublicMemberMap(_environment._variableNodes.first), diff --git a/lib/src/module.dart b/lib/src/module.dart index 63402758..e7b9a90b 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_module.dart. // See tool/synchronize.dart for details. // -// Checksum: 5608be0fdb1bff974611b75d1bbcb364a15d4df2 +// Checksum: 759037174212e69cd0d9a7ebf55f6ee9f66072e8 // // ignore_for_file: unused_import @@ -18,6 +18,9 @@ import 'value.dart'; /// The interface for a Sass module. abstract class Module { + /// Modules that this module uses. + List get upstream; + /// The module's variables. Map get variables; diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index fe21c9a1..baa5ffaf 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -242,7 +242,7 @@ class _EvaluateVisitor var module = await _execute(importer, node); _extender.finalize(); - return EvaluateResult(module.css, _includedFiles); + return EvaluateResult(_combineCss(module), _includedFiles); } Future runExpression(Expression expression, @@ -406,6 +406,47 @@ class _EvaluateVisitor return CssStylesheet(statements.build(), _root.span); } + /// Returns a new stylesheet containing [root]'s CSS as well as the CSS of all + /// modules transitively used by [root]. + CssStylesheet _combineCss(AsyncModule root) { + if (root.upstream.isEmpty) return root.css; + + var seen = Set(); + var imports = []; + var css = []; + + void visitModule(AsyncModule module) { + if (!seen.add(module)) return; + for (var module in module.upstream) { + visitModule(module); + } + + var statements = module.css.children; + var index = _indexAfterImports(statements); + imports.addAll(statements.getRange(0, index)); + css.addAll(statements.getRange(index, statements.length)); + } + + visitModule(root); + + return CssStylesheet(imports + css, root.css.span); + } + + /// Returns the index of the first node in [statements] that comes after all + /// static imports. + int _indexAfterImports(List statements) { + var lastImport = -1; + for (var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement is CssImport) { + lastImport = i; + } else if (statement is! CssComment) { + break; + } + } + return lastImport + 1; + } + // ## Statements Future visitStylesheet(Stylesheet node) async { diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 0cc53aa6..335b9141 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/synchronize.dart for details. // -// Checksum: ae0454752ee85a9094d0f0d0e6e8a252364b70a1 +// Checksum: da6068544fd58007ee2d44c2688063cbe4543e73 // // ignore_for_file: unused_import @@ -249,7 +249,7 @@ class _EvaluateVisitor var module = _execute(importer, node); _extender.finalize(); - return EvaluateResult(module.css, _includedFiles); + return EvaluateResult(_combineCss(module), _includedFiles); } Value runExpression(Expression expression, {Map variables}) { @@ -412,6 +412,47 @@ class _EvaluateVisitor return CssStylesheet(statements.build(), _root.span); } + /// Returns a new stylesheet containing [root]'s CSS as well as the CSS of all + /// modules transitively used by [root]. + CssStylesheet _combineCss(Module root) { + if (root.upstream.isEmpty) return root.css; + + var seen = Set(); + var imports = []; + var css = []; + + void visitModule(Module module) { + if (!seen.add(module)) return; + for (var module in module.upstream) { + visitModule(module); + } + + var statements = module.css.children; + var index = _indexAfterImports(statements); + imports.addAll(statements.getRange(0, index)); + css.addAll(statements.getRange(index, statements.length)); + } + + visitModule(root); + + return CssStylesheet(imports + css, root.css.span); + } + + /// Returns the index of the first node in [statements] that comes after all + /// static imports. + int _indexAfterImports(List statements) { + var lastImport = -1; + for (var i = 0; i < statements.length; i++) { + var statement = statements[i]; + if (statement is CssImport) { + lastImport = i; + } else if (statement is! CssComment) { + break; + } + } + return lastImport + 1; + } + // ## Statements Value visitStylesheet(Stylesheet node) {