Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ All submissions, including submissions by project members, require review.
### File headers
All files in the project must start with the following header.

// Copyright 2018 Google LLC
// Copyright 2019 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
Expand Down
9 changes: 9 additions & 0 deletions bin/sass_migrator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2019 Google LLC
//
// 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:sass_migrator/runner.dart';

main(List<String> args) => MigratorRunner().execute(args);
58 changes: 0 additions & 58 deletions bin/sass_module_migrator.dart

This file was deleted.

63 changes: 63 additions & 0 deletions lib/runner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2019 Google LLC
//
// 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:io';

import 'package:args/command_runner.dart';
import 'package:path/path.dart' as p;

import 'src/migrators/module.dart';

/// A command runner that runs a migrator based on provided arguments.
class MigratorRunner extends CommandRunner<Map<Uri, String>> {
final invocation = "sass_migrator <migrator> [options] <entrypoint.scss...>";

MigratorRunner()
: super("sass_migrator", "Migrates stylesheets to new Sass versions.") {
argParser.addFlag('migrate-deps',
abbr: 'd', help: 'Migrate dependencies in addition to entrypoints.');
argParser.addFlag('dry-run',
abbr: 'n',
help: 'Show which files would be migrated but make no changes.');
// TODO(jathak): Make this flag print a diff instead.
argParser.addFlag('verbose',
abbr: 'v',
help: 'Print text of migrated files when running with --dry-run.');
addCommand(ModuleMigrator());
}

/// Runs a migrator and then writes the migrated files to disk unless
/// `--dry-run` is passed.
Future execute(Iterable<String> args) async {
var argResults = parse(args);
var migrated = await runCommand(argResults);
if (migrated == null) return;

if (migrated.isEmpty) {
print('Nothing to migrate!');
return;
}

if (argResults['dry-run']) {
print('Dry run. Logging migrated files instead of overwriting...\n');
for (var url in migrated.keys) {
print(p.prettyUri(url));
if (argResults['verbose']) {
print('=' * 80);
print(migrated[url]);
print('-' * 80);
}
}
} else {
for (var url in migrated.keys) {
assert(url.scheme == null || url.scheme == "file",
"$url is not a file path.");
if (argResults['verbose']) print("Overwriting $url...");
File(url.toFilePath()).writeAsStringSync(migrated[url]);
}
}
}
}
114 changes: 114 additions & 0 deletions lib/src/migration_visitor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2019 Google LLC
//
// 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:collection';

// The sass package's API is not necessarily stable. It is being imported with
// the Sass team's explicit knowledge and approval. See
// https://github.com/sass/dart-sass/issues/236.
import 'package:sass/src/ast/sass.dart';
import 'package:sass/src/visitor/recursive_ast.dart';

import 'package:meta/meta.dart';

import 'patch.dart';
import 'utils.dart';

/// A visitor that migrates a stylesheet.
///
/// When [run] is called, this visitor traverses a stylesheet's AST, allowing
/// subclasses to override one or more methods and add to [patches]. Once the
/// stylesheet has been visited, the migrated contents (based on [patches]) will
/// be stored in [migrator]'s [migrated] map.
///
/// If [migrateDependencies] is enabled, this visitor will construct and run a
/// new instance of itself (using [newInstance]) each time it encounters an
/// `@import` or `@use` rule.
abstract class MigrationVisitor extends RecursiveAstVisitor {
/// A mapping from URLs to migrated contents for stylesheets already migrated.
final _migrated = <Uri, String>{};

/// True if dependencies should be migrated as well.
final bool migrateDependencies;

/// The patches to be applied to the stylesheet being migrated.
UnmodifiableListView<Patch> get patches => UnmodifiableListView(_patches);
List<Patch> _patches;

MigrationVisitor({this.migrateDependencies = true});

/// Runs a new migration on [url] (and its dependencies, if
/// [migrateDependencies] is true) and returns a map of migrated contents.
Map<Uri, String> run(Uri url) {
visitStylesheet(parseStylesheet(url));
return _migrated;
}

/// Visits stylesheet starting with an empty [_patches], adds the migrated
/// contents (if any) to [_migrated], and then restores the previous value of
/// [_patches].
///
/// Migrators with per-file state should override this to store the current
/// file's state before calling the super method and restore it afterwards.
@override
void visitStylesheet(Stylesheet node) {
var oldPatches = _patches;
_patches = [];
super.visitStylesheet(node);
var results = getMigratedContents();
if (results != null) {
_migrated[node.span.sourceUrl] = results;
}
_patches = oldPatches;
}

/// Visits the stylesheet at [dependency], resolved relative to [source].
@protected
void visitDependency(Uri dependency, Uri source) {
var stylesheet = parseStylesheet(source.resolveUri(dependency));
visitStylesheet(stylesheet);
}

/// Returns the migrated contents of this file, or null if the file does not
/// change.
///
/// This will be called by [run] and the results will be stored in
/// `migrator.migrated`.
@protected
String getMigratedContents() => patches.isNotEmpty
? Patch.applyAll(patches.first.selection.file, patches)
: null;

/// Adds a new patch that should be applied to the current stylesheet.
@protected
void addPatch(Patch patch) {
_patches.add(patch);
}

/// If [migrateDependencies] is enabled, any dynamic imports within
/// this [node] will be migrated before continuing.
@override
visitImportRule(ImportRule node) {
super.visitImportRule(node);
if (migrateDependencies) {
for (var import in node.imports) {
if (import is DynamicImport) {
visitDependency(Uri.parse(import.url), node.span.sourceUrl);
}
}
}
}

/// If [migrateDependencies] is enabled, this dependency will be
/// migrated before continuing.
@override
visitUseRule(UseRule node) {
super.visitUseRule(node);
if (migrateDependencies) {
visitDependency(node.url, node.span.sourceUrl);
}
}
}
Loading