From 7900dda573c2cc24334d273b05030a4d213c9e10 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 28 Jul 2021 18:28:00 -0700 Subject: [PATCH] Add a sass_api package and publishing infrastructure --- .github/workflows/ci.yml | 29 +++++++-- .gitignore | 1 + CONTRIBUTING.md | 25 ++++++++ README.md | 10 +++ pkg/sass_api/CHANGELOG.md | 3 + pkg/sass_api/README.md | 29 +++++++++ pkg/sass_api/dartdoc_options.yaml | 18 ++++++ pkg/sass_api/doc/ast.md | 6 ++ pkg/sass_api/doc/compile.md | 1 + pkg/sass_api/doc/dependencies.md | 1 + pkg/sass_api/doc/importer.md | 1 + pkg/sass_api/doc/parsing.md | 1 + pkg/sass_api/doc/value.md | 1 + pkg/sass_api/doc/visitor.md | 10 +++ pkg/sass_api/lib/sass_api.dart | 36 +++++++++++ pkg/sass_api/pubspec.yaml | 17 +++++ pubspec.yaml | 6 +- test/double_check_test.dart | 103 +++++++++++++++++++++++++----- test/repo_test.dart | 5 +- tool/grind.dart | 8 +-- tool/grind/subpackages.dart | 82 ++++++++++++++++++++++++ 21 files changed, 364 insertions(+), 29 deletions(-) create mode 100644 pkg/sass_api/CHANGELOG.md create mode 100644 pkg/sass_api/README.md create mode 100644 pkg/sass_api/dartdoc_options.yaml create mode 100644 pkg/sass_api/doc/ast.md create mode 120000 pkg/sass_api/doc/compile.md create mode 100644 pkg/sass_api/doc/dependencies.md create mode 120000 pkg/sass_api/doc/importer.md create mode 100644 pkg/sass_api/doc/parsing.md create mode 120000 pkg/sass_api/doc/value.md create mode 100644 pkg/sass_api/doc/visitor.md create mode 100644 pkg/sass_api/lib/sass_api.dart create mode 100644 pkg/sass_api/pubspec.yaml create mode 100644 tool/grind/subpackages.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0d54914f..0fc4b98e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,11 +121,9 @@ jobs: steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v1 - # TODO(nweiz): Use the latest Dart when dart-lang/sdk#45488 - with: {sdk: 2.12.4} - run: dart pub get - - name: Analyze dart - run: dartanalyzer --fatal-warnings --fatal-infos lib tool test + - name: Analyze Dart + run: dart analyze --fatal-warnings --fatal-infos . dartdoc: name: Dartdoc @@ -135,10 +133,14 @@ jobs: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v1 - run: dart pub get - - name: Run dartdoc + - name: dartdoc sass run: dartdoc --quiet --no-generate-docs --errors ambiguous-doc-reference,broken-link,deprecated --errors unknown-directive,unknown-macro,unresolved-doc-reference + - name: dartdoc sass_api + run: cd pkg/sass_api && dartdoc --quiet --no-generate-docs + --errors ambiguous-doc-reference,broken-link,deprecated + --errors unknown-directive,unknown-macro,unresolved-doc-reference sanity_checks: name: Sanity checks @@ -252,6 +254,23 @@ jobs: run: dart pub run grinder pkg-pub-deploy env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} + deploy_sub_packages: + name: "Deploy Sub-Packages" + runs-on: ubuntu-latest + needs: [deploy_pub] + if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" + + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v1 + - run: dart pub get + - name: Deploy + run: dart pub run grinder deploy-sub-packages + env: + PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}" + GH_TOKEN: "${{ secrets.GH_TOKEN }}" + GH_USER: sassbot + deploy_homebrew: name: "Deploy Homebrew" runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 40f791a7e..89b6acfd6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ package-lock.json /benchmark/source node_modules/ /doc/api +/pkg/*/doc/api diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e03350157..8f252cf1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,7 @@ Want to contribute? Great! First, read this page. * [Changing the Node API](#changing-the-node-api) * [Synchronizing](#synchronizing) * [File Headers](#file-headers) +* [Release Process](#release-process) ## Before You Contribute @@ -208,3 +209,27 @@ All files in the project must start with the following header. // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. ``` + +## Release Process + +Most of the release process is fully automated on GitHub actions, triggered by +pushing a tag matching the current `pubspec.yaml` version. However, there are a +few things to do before pushing that tag: + +* Make sure the `pubspec.yaml` version doesn't end in `-dev`. (This is a Dart + convention to distinguish commits that aren't meant for release from commits + that are.) + +* Make sure that `CHANGELOG.md` has an entry for the current version. + +* Make sure that any packages in `pkg` depend on the current version of `sass`. + +* Increment the versions of all packages in `pkg`. These should be incremented + at least as much as the `sass` version, and more if you add a new API that's + exposed by one of those packages. + +* Make sure that every package in `pkg`'s `CHANGELOG.md` has an entry for its + current version. + +You *don't* need to create tags for packages in `pkg`; that will be handled +automatically by GitHub actions. diff --git a/README.md b/README.md index e516c92c3..0ef5b68aa 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun again**. * [Standalone](#standalone) * [From npm](#from-npm) * [From Pub](#from-pub) + * [`sass_api` Package](#sass_api-package) * [From Source](#from-source) * [JavaScript API](#javascript-api) * [Why Dart?](#why-dart) @@ -129,6 +130,15 @@ See [the Dart API docs][api] for details. [api]: https://www.dartdocs.org/documentation/sass/latest/sass/sass-library.html +#### `sass_api` Package + +Dart users also have access to more in-depth APIs via the [`sass_api` package]. +This provides access to the Sass AST and APIs for resolving Sass loads without +running a full compilation. It's separated out into its own package so that it +can increase its version number independently of the main `sass` package. + +[`sass_api` package]: https://pub.dev/package/sass_api + ### From Source Assuming you've already checked out this repository: diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md new file mode 100644 index 000000000..fece6e1ce --- /dev/null +++ b/pkg/sass_api/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0-beta.1 + +* Initial beta release. diff --git a/pkg/sass_api/README.md b/pkg/sass_api/README.md new file mode 100644 index 000000000..a74a9f308 --- /dev/null +++ b/pkg/sass_api/README.md @@ -0,0 +1,29 @@ +This package exposes additional APIs for working with [Dart Sass], including +access to the Sass AST and its load resolution logic. + +[Dart Sass]: https://pub.dev/packages/sass + +This is split out into a separate package because so that it can be versioned +separately. The `sass_api` package's API is expected to evolve more quickly than +the Sass language itself, and will likely have more breaking changes as the +internals evolve to suit the needs of the Sass compiler. + +## Depending on Development Versions + +Sometimes it's necessary to depend on a version of a package that hasn't been +released yet. Because this package directly re-exports names from the main +`sass` package, you'll need to make sure you have a Git dependency on both it +*and* the `sass` package: + +```yaml +dependency_overrides: + sass: + git: + url: git://github.com/sass/sass + ref: main # Replace this with a feature branch if necessary + sass_api: + git: + url: git://github.com/sass/sass + ref: main # Make sure this is the same as above! + path: pkg/sass_api +``` diff --git a/pkg/sass_api/dartdoc_options.yaml b/pkg/sass_api/dartdoc_options.yaml new file mode 100644 index 000000000..27d3de68b --- /dev/null +++ b/pkg/sass_api/dartdoc_options.yaml @@ -0,0 +1,18 @@ +dartdoc: + categories: + AST: + markdown: doc/ast.md + name: Abstract Syntax Tree + Dependencies: + markdown: doc/dependencies.md + Importer: + markdown: doc/importer.md + Parsing: + markdown: doc/parsing.md + Compile: + markdown: doc/compile.md + Value: + markdown: doc/value.md + Visitor: + markdown: doc/visitor.md + ignore: [reexported-private-api-across-packages] diff --git a/pkg/sass_api/doc/ast.md b/pkg/sass_api/doc/ast.md new file mode 100644 index 000000000..48524800d --- /dev/null +++ b/pkg/sass_api/doc/ast.md @@ -0,0 +1,6 @@ +Classes representing Sass's abstract syntax tree. + +Certain AST classes, most notably [`Stylesheet`], have `parse()` constructors +that parse ASTs from string sources. + +[`Stylesheet`]: ../sass/Stylesheet-class.html diff --git a/pkg/sass_api/doc/compile.md b/pkg/sass_api/doc/compile.md new file mode 120000 index 000000000..842c3ce62 --- /dev/null +++ b/pkg/sass_api/doc/compile.md @@ -0,0 +1 @@ +../../../doc/compile.md \ No newline at end of file diff --git a/pkg/sass_api/doc/dependencies.md b/pkg/sass_api/doc/dependencies.md new file mode 100644 index 000000000..fca23e502 --- /dev/null +++ b/pkg/sass_api/doc/dependencies.md @@ -0,0 +1 @@ +APIs for resolving dependencies between Sass files. diff --git a/pkg/sass_api/doc/importer.md b/pkg/sass_api/doc/importer.md new file mode 120000 index 000000000..67f7de4ef --- /dev/null +++ b/pkg/sass_api/doc/importer.md @@ -0,0 +1 @@ +../../../doc/importer.md \ No newline at end of file diff --git a/pkg/sass_api/doc/parsing.md b/pkg/sass_api/doc/parsing.md new file mode 100644 index 000000000..b1527975b --- /dev/null +++ b/pkg/sass_api/doc/parsing.md @@ -0,0 +1 @@ +APIs that parse Sass or CSS source. diff --git a/pkg/sass_api/doc/value.md b/pkg/sass_api/doc/value.md new file mode 120000 index 000000000..fb98293f0 --- /dev/null +++ b/pkg/sass_api/doc/value.md @@ -0,0 +1 @@ +../../../doc/value.md \ No newline at end of file diff --git a/pkg/sass_api/doc/visitor.md b/pkg/sass_api/doc/visitor.md new file mode 100644 index 000000000..a28f2f9d3 --- /dev/null +++ b/pkg/sass_api/doc/visitor.md @@ -0,0 +1,10 @@ +Classes that implement the [visitor pattern] for traversing the Sass [AST]. +Callers can either implement interfaces like [`StatementVisitor`] from scratch +to handle *all* Sass node types, or extend helper classes like +[`RecursiveStatementVisitor`] which traverse the entire AST to handle only +specific nodes. + +[visitor pattern]: https://en.wikipedia.org/wiki/Visitor_pattern +[AST]: AST-topic.html +[`StatementVisitor`]: ../sass/StatementVisitor-class.html +[`RecursiveStatementVisitor`]: ../sass/RecursiveStatementVisitor-class.html diff --git a/pkg/sass_api/lib/sass_api.dart b/pkg/sass_api/lib/sass_api.dart new file mode 100644 index 000000000..4f680a8e5 --- /dev/null +++ b/pkg/sass_api/lib/sass_api.dart @@ -0,0 +1,36 @@ +// 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. + +/// We strongly recommend importing this library with the prefix `sass`. +library sass; + +import 'package:sass/sass.dart'; +import 'package:sass/src/parse/parser.dart'; + +export 'package:sass/sass.dart'; +export 'package:sass/src/ast/node.dart'; +export 'package:sass/src/ast/sass.dart' hide AtRootQuery; +export 'package:sass/src/async_import_cache.dart'; +export 'package:sass/src/exception.dart' show SassFormatException; +export 'package:sass/src/import_cache.dart'; +export 'package:sass/src/visitor/find_dependencies.dart'; +export 'package:sass/src/visitor/interface/expression.dart'; +export 'package:sass/src/visitor/interface/statement.dart'; +export 'package:sass/src/visitor/recursive_ast.dart'; +export 'package:sass/src/visitor/recursive_statement.dart'; +export 'package:sass/src/visitor/statement_search.dart'; + +/// Parses [text] as a CSS identifier and returns the result. +/// +/// Throws a [SassFormatException] if parsing fails. +/// +/// {@category Parsing} +String parseIdentifier(String text) => + Parser.parseIdentifier(text, logger: Logger.quiet); + +/// Returns whether [text] is a valid CSS identifier. +/// +/// {@category Parsing} +bool isIdentifier(String text) => + Parser.isIdentifier(text, logger: Logger.quiet); diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml new file mode 100644 index 000000000..433b44703 --- /dev/null +++ b/pkg/sass_api/pubspec.yaml @@ -0,0 +1,17 @@ +name: sass_api +# Note: Every time we add a new Sass AST node, we need to bump the *major* +# version because it's a breaking change for anyone who's implementing the +# visitor interface(s). +version: 1.0.0-beta.1 +description: Additional APIs for Dart Sass. +author: Sass Team +homepage: https://github.com/sass/dart-sass + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + sass: 1.37.0 + +dependency_overrides: + sass: {path: ../..} diff --git a/pubspec.yaml b/pubspec.yaml index 0a8b08fa3..261759fb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,10 +30,11 @@ dependencies: term_glyph: ^1.2.0 tuple: ^2.0.0 watcher: ^1.0.0 + http: ^0.13.3 dev_dependencies: - archive: ^3.1.2 analyzer: ^1.1.0 + archive: ^3.1.2 cli_pkg: ^1.3.0 crypto: ^3.0.0 dart_style: ^2.0.0 @@ -41,8 +42,9 @@ dev_dependencies: node_preamble: ^2.0.0 pedantic: ^1.11.0 pub_semver: ^2.0.0 + pubspec_parse: ^1.0.0 stream_channel: ^2.1.0 + test: ^1.16.7 test_descriptor: ^2.0.0 test_process: ^2.0.0 - test: ^1.16.7 yaml: ^3.1.0 diff --git a/test/double_check_test.dart b/test/double_check_test.dart index 5b9d38445..33d6b969b 100644 --- a/test/double_check_test.dart +++ b/test/double_check_test.dart @@ -8,8 +8,10 @@ import 'dart:io'; import 'dart:convert'; import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; -import 'package:yaml/yaml.dart'; import '../tool/grind/synchronize.dart' as synchronize; @@ -38,19 +40,88 @@ void main() { // newline normalization issues. testOn: "!windows"); - test("pubspec version matches CHANGELOG version", () { - var firstLine = const LineSplitter() - .convert(File("CHANGELOG.md").readAsStringSync()) - .first; - expect(firstLine, startsWith("## ")); - var changelogVersion = firstLine.substring(3); - - var pubspec = loadYaml(File("pubspec.yaml").readAsStringSync(), - sourceUrl: Uri(path: "pubspec.yaml")) as Map; - expect(pubspec, containsPair("version", isA())); - var pubspecVersion = pubspec["version"] as String; - - expect(pubspecVersion, - anyOf(equals(changelogVersion), equals("$changelogVersion-dev"))); - }); + for (var package in [ + ".", + ...Directory("pkg").listSync().map((entry) => entry.path) + ]) { + group("in ${p.relative(package)}", () { + test("pubspec version matches CHANGELOG version", () { + var firstLine = const LineSplitter() + .convert(File("$package/CHANGELOG.md").readAsStringSync()) + .first; + expect(firstLine, startsWith("## ")); + var changelogVersion = firstLine.substring(3); + + var pubspec = Pubspec.parse( + File("$package/pubspec.yaml").readAsStringSync(), + sourceUrl: p.toUri("$package/pubspec.yaml")); + expect(pubspec.version!.toString(), + anyOf(equals(changelogVersion), equals("$changelogVersion-dev"))); + }); + }); + } + + for (var package in Directory("pkg").listSync().map((entry) => entry.path)) { + group("in pkg/${p.basename(package)}", () { + late Pubspec sassPubspec; + late Pubspec pkgPubspec; + setUpAll(() { + sassPubspec = Pubspec.parse(File("pubspec.yaml").readAsStringSync(), + sourceUrl: Uri.parse("pubspec.yaml")); + pkgPubspec = Pubspec.parse( + File("$package/pubspec.yaml").readAsStringSync(), + sourceUrl: p.toUri("$package/pubspec.yaml")); + }); + + test("depends on the current sass version", () { + if (_isDevVersion(sassPubspec.version!)) return; + + expect(pkgPubspec.dependencies, contains("sass")); + var dependency = pkgPubspec.dependencies["sass"]!; + expect(dependency, isA()); + expect((dependency as HostedDependency).version, + equals(sassPubspec.version)); + }); + + test("increments along with the sass version", () { + var sassVersion = sassPubspec.version!; + if (_isDevVersion(sassVersion)) return; + + var pkgVersion = pkgPubspec.version!; + expect(_isDevVersion(pkgVersion), isFalse, + reason: "sass $sassVersion isn't a dev version but " + "${pkgPubspec.name} $pkgVersion is"); + + if (sassVersion.isPreRelease) { + expect(pkgVersion.isPreRelease, isTrue, + reason: "sass $sassVersion is a pre-release version but " + "${pkgPubspec.name} $pkgVersion isn't"); + } + + // If only sass's patch version was incremented, there's not a good way + // to tell whether the sub-package's version was incremented as well + // because we don't have access to the prior version. + if (sassVersion.patch != 0) return; + + if (sassVersion.minor != 0) { + expect(pkgVersion.patch, equals(0), + reason: "sass minor version was incremented, ${pkgPubspec.name} " + "must increment at least its minor version"); + } else { + expect(pkgVersion.minor, equals(0), + reason: "sass major version was incremented, ${pkgPubspec.name} " + "must increment at its major version as well"); + } + }); + + test("matches SDK version", () { + expect(pkgPubspec.environment!["sdk"], + equals(sassPubspec.environment!["sdk"])); + }); + }); + } } + +/// Returns whether [version] is a `-dev` version. +bool _isDevVersion(Version version) => + version.preRelease.length == 1 && version.preRelease.first == 'dev'; diff --git a/test/repo_test.dart b/test/repo_test.dart index ff90d6188..54d89fdbc 100644 --- a/test/repo_test.dart +++ b/test/repo_test.dart @@ -12,7 +12,10 @@ import 'package:yaml/yaml.dart'; void main() { group("YAML files are valid:", () { - for (var entry in Directory.current.listSync()) { + for (var entry in [ + ...Directory.current.listSync(), + ...Directory("pkg").listSync(recursive: true) + ]) { if (entry is File && (entry.path.endsWith(".yaml") || entry.path.endsWith(".yml"))) { test(p.basename(entry.path), () { diff --git a/tool/grind.dart b/tool/grind.dart index 1162cf0f9..8060773be 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -16,6 +16,7 @@ import 'grind/synchronize.dart'; export 'grind/bazel.dart'; export 'grind/benchmark.dart'; export 'grind/sanity_check.dart'; +export 'grind/subpackages.dart'; export 'grind/synchronize.dart'; void main(List args) { @@ -58,11 +59,8 @@ void all() {} @Task('Run the Dart formatter.') void format() { - Pub.run('dart_style', script: 'format', arguments: [ - '--overwrite', - '--fix', - for (var dir in existingSourceDirs) dir.path - ]); + Pub.run('dart_style', + script: 'format', arguments: ['--overwrite', '--fix', '.']); } @Task('Installs dependencies from npm.') diff --git a/tool/grind/subpackages.dart b/tool/grind/subpackages.dart new file mode 100644 index 000000000..6c77573c1 --- /dev/null +++ b/tool/grind/subpackages.dart @@ -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 'dart:io'; +import 'dart:convert'; + +import 'package:cli_pkg/cli_pkg.dart' as pkg; +import 'package:grinder/grinder.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:pubspec_parse/pubspec_parse.dart'; + +/// The path in which pub expects to find its credentials file. +final String _pubCredentialsPath = () { + // This follows the same logic as pub: + // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 + String cacheDir; + var pubCache = Platform.environment['PUB_CACHE']; + if (pubCache != null) { + cacheDir = pubCache; + } else if (Platform.isWindows) { + var appData = Platform.environment['APPDATA']!; + cacheDir = p.join(appData, 'Pub', 'Cache'); + } else { + cacheDir = p.join(Platform.environment['HOME']!, '.pub-cache'); + } + + return p.join(cacheDir, 'credentials.json'); +}(); + +/// Returns the HTTP basic authentication Authorization header from the +/// environment. +String get _githubAuthorization { + var bearerToken = pkg.githubBearerToken.value; + return bearerToken != null + ? "Bearer $bearerToken" + : "Basic " + + base64.encode(utf8.encode("${pkg.githubUser}:${pkg.githubPassword}")); +} + +@Task('Deploy sub-packages to pub.') +Future deploySubPackages() async { + // Write pub credentials + Directory(p.dirname(_pubCredentialsPath)).createSync(recursive: true); + File(_pubCredentialsPath).openSync(mode: FileMode.writeOnlyAppend) + ..writeStringSync(pkg.pubCredentials.value) + ..closeSync(); + + var client = http.Client(); + for (var package in Directory("pkg").listSync().map((dir) => dir.path)) { + var pubspec = Pubspec.parse( + File("$package/pubspec.yaml").readAsStringSync(), + sourceUrl: p.toUri("$package/pubspec.yaml")); + + log("pub publish ${pubspec.name}"); + var process = await Process.start( + p.join(sdkDir.path, "bin/pub"), ["publish", "--force"], + workingDirectory: package); + LineSplitter().bind(utf8.decoder.bind(process.stdout)).listen(log); + LineSplitter().bind(utf8.decoder.bind(process.stderr)).listen(log); + if (await process.exitCode != 0) fail("pub publish ${pubspec.name} failed"); + + var response = await client.post( + Uri.parse("https://api.github.com/repos/sass/sass/git/refs"), + headers: { + "accept": "application/vnd.github.v3+json", + "content-type": "application/json", + "authorization": _githubAuthorization + }, + body: jsonEncode({ + "ref": "refs/tags/${pubspec.name}/${pubspec.version}", + "sha": Platform.environment["GITHUB_SHA"]! + })); + + if (response.statusCode != 201) { + fail("${response.statusCode} error creating tag:\n${response.body}"); + } else { + log("Tagged ${pubspec.name} ${pubspec.version}."); + } + } +}