├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin └── dart_sass_embedded.dart ├── buf.gen.yaml ├── buf.work.yaml ├── lib └── src │ ├── dispatcher.dart │ ├── function_registry.dart │ ├── host_callable.dart │ ├── importer │ ├── base.dart │ ├── file.dart │ └── host.dart │ ├── logger.dart │ ├── protofier.dart │ ├── util │ └── length_delimited_transformer.dart │ ├── utils.dart │ └── value.dart ├── pubspec.lock ├── pubspec.yaml ├── test ├── dependencies_test.dart ├── embedded_process.dart ├── file_importer_test.dart ├── function_test.dart ├── importer_test.dart ├── length_delimited_test.dart ├── protocol_test.dart └── utils.dart └── tool ├── grind.dart └── utils.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pub" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | defaults: 4 | run: {shell: bash} 5 | 6 | on: 7 | push: 8 | branches: [main, feature.*] 9 | tags: ['**'] 10 | pull_request: 11 | 12 | jobs: 13 | dart_tests: 14 | name: "Dart tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.os }}" 15 | runs-on: "${{ matrix.os }}" 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-latest, windows-latest, macos-latest] 21 | dart_channel: [stable] 22 | include: [{os: ubuntu-latest, dart_channel: dev}] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: frenck/action-setup-yq@v1 27 | with: {version: v4.30.5} # frenck/action-setup-yq#35 28 | - uses: bufbuild/buf-setup-action@v1.13.1 29 | with: {github_token: "${{ github.token }}"} 30 | - uses: dart-lang/setup-dart@v1 31 | with: {sdk: "${{ matrix.dart_channel }}"} 32 | 33 | - name: Check out Dart Sass only if linked in the PR description 34 | uses: sass/clone-linked-repo@v1 35 | with: 36 | repo: sass/dart-sass 37 | path: build/dart-sass 38 | default-ref: null 39 | 40 | - name: Link the embedded compiler to Dart Sass 41 | run: | 42 | if [[ -d build/dart-sass ]]; then 43 | yq -i ' 44 | .dependency_overrides.sass = {"path": "build/dart-sass"} | 45 | .dependency_overrides.sass_api = {"path": "build/dart-sass/pkg/sass_api"} 46 | ' pubspec.yaml 47 | fi 48 | shell: bash 49 | 50 | - name: Check out embedded Sass protocol 51 | uses: sass/clone-linked-repo@v1 52 | with: {repo: sass/embedded-protocol, path: build/embedded-protocol} 53 | 54 | - run: dart pub get 55 | - run: dart run grinder protobuf 56 | env: {UPDATE_SASS_PROTOCOL: false} 57 | - run: dart run grinder pkg-standalone-dev 58 | - name: Run tests 59 | run: dart run test -r expanded 60 | 61 | # The versions should be kept up-to-date with the latest LTS Node releases. 62 | # They next need to be rotated October 2021. See 63 | # https://github.com/nodejs/Release. 64 | sass_spec: 65 | name: 'JS API Tests | Node ${{ matrix.node_version }} | ${{ matrix.os }}' 66 | runs-on: ${{ matrix.os }}-latest 67 | 68 | strategy: 69 | fail-fast: false 70 | matrix: 71 | os: [ubuntu, windows, macos] 72 | node_version: [18] 73 | include: 74 | # Include LTS versions on Ubuntu 75 | - os: ubuntu 76 | node_version: 16 77 | - os: ubuntu 78 | node_version: 14 79 | 80 | steps: 81 | - uses: actions/checkout@v3 82 | - uses: dart-lang/setup-dart@v1 83 | with: {sdk: stable} 84 | - uses: frenck/action-setup-yq@v1 85 | with: {version: v4.30.5} # frenck/action-setup-yq#35 86 | - uses: bufbuild/buf-setup-action@v1.13.1 87 | with: {github_token: "${{ github.token }}"} 88 | 89 | - name: Check out Dart Sass only if linked in the PR description 90 | uses: sass/clone-linked-repo@v1 91 | with: 92 | repo: sass/dart-sass 93 | path: build/dart-sass 94 | # Unless we're cutting a release, run the main branch of Dart Sass 95 | # against the main branch of sass-spec so that we don't need to make 96 | # an empty commit to this repo every time we update those two. 97 | default-ref: ${{ !startsWith(github.ref, 'refs/tags/') && 'main' || null }} 98 | 99 | - name: Link the embedded compiler to Dart Sass 100 | run: | 101 | if [[ -d build/dart-sass ]]; then 102 | yq -i ' 103 | .dependency_overrides.sass = {"path": "build/dart-sass"} | 104 | .dependency_overrides.sass_api = {"path": "build/dart-sass/pkg/sass_api"} 105 | ' pubspec.yaml 106 | fi 107 | shell: bash 108 | 109 | - name: Check out embedded Sass protocol 110 | uses: sass/clone-linked-repo@v1 111 | with: {repo: sass/embedded-protocol, path: build/embedded-protocol} 112 | 113 | - name: Check out the embedded host 114 | uses: sass/clone-linked-repo@v1 115 | with: {repo: sass/embedded-host-node} 116 | 117 | - name: Check out the JS API definition 118 | uses: sass/clone-linked-repo@v1 119 | with: {repo: sass/sass, path: language} 120 | 121 | - name: Initialize embedded host 122 | run: | 123 | npm install 124 | npm run init -- --protocol-path=../build/embedded-protocol \ 125 | --compiler-path=.. --api-path=../language 126 | npm run compile 127 | mv {`pwd`/,dist/}lib/src/vendor/dart-sass-embedded 128 | working-directory: embedded-host-node 129 | 130 | - name: Check out sass-spec 131 | uses: sass/clone-linked-repo@v1 132 | with: {repo: sass/sass-spec} 133 | 134 | - name: Install sass-spec dependencies 135 | run: npm install 136 | working-directory: sass-spec 137 | 138 | - name: Version info 139 | run: | 140 | path=embedded-host-node/dist/lib/src/vendor/dart-sass-embedded/dart-sass-embedded 141 | if [[ -f "$path.cmd" ]]; then "./$path.cmd" --version 142 | elif [[ -f "$path.bat" ]]; then "./$path.bat" --version 143 | elif [[ -f "$path.exe" ]]; then "./$path.exe" --version 144 | else "./$path" --version 145 | fi 146 | 147 | - name: Run tests 148 | run: npm run js-api-spec -- --sassPackage ../embedded-host-node --sassSassRepo ../language 149 | working-directory: sass-spec 150 | 151 | static_analysis: 152 | name: Static analysis 153 | runs-on: ubuntu-latest 154 | 155 | steps: 156 | - uses: actions/checkout@v3 157 | - uses: frenck/action-setup-yq@v1 158 | with: {version: v4.30.5} # frenck/action-setup-yq#35 159 | - uses: bufbuild/buf-setup-action@v1.13.1 160 | with: {github_token: "${{ github.token }}"} 161 | - uses: dart-lang/setup-dart@v1 162 | 163 | - name: Check out Dart Sass only if linked in the PR description 164 | uses: sass/clone-linked-repo@v1 165 | with: 166 | repo: sass/dart-sass 167 | path: build/dart-sass 168 | default-ref: null 169 | 170 | - name: Link the embedded compiler to Dart Sass 171 | run: | 172 | if [[ -d build/dart-sass ]]; then 173 | yq -i ' 174 | .dependency_overrides.sass = {"path": "build/dart-sass"} | 175 | .dependency_overrides.sass_api = {"path": "build/dart-sass/pkg/sass_api"} 176 | ' pubspec.yaml 177 | fi 178 | shell: bash 179 | 180 | - name: Check out embedded Sass protocol 181 | uses: sass/clone-linked-repo@v1 182 | with: {repo: sass/embedded-protocol, path: build/embedded-protocol} 183 | 184 | - run: dart pub get 185 | - run: dart run grinder protobuf 186 | env: {UPDATE_SASS_PROTOCOL: false} 187 | - name: Analyze dart 188 | run: dart analyze --fatal-warnings ./ 189 | 190 | format: 191 | name: Code formatting 192 | runs-on: ubuntu-latest 193 | 194 | steps: 195 | - uses: actions/checkout@v3 196 | - uses: dart-lang/setup-dart@v1 197 | - run: dart format --fix . 198 | - run: git diff --exit-code 199 | 200 | deploy_github_linux: 201 | name: "Deploy Github: linux-ia32, linux-x64" 202 | runs-on: ubuntu-latest 203 | needs: [dart_tests, sass_spec, static_analysis, format] 204 | if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass-embedded'" 205 | 206 | steps: 207 | - uses: actions/checkout@v3 208 | - uses: bufbuild/buf-setup-action@v1.13.1 209 | with: {github_token: "${{ github.token }}"} 210 | - uses: dart-lang/setup-dart@v1 211 | - run: dart pub get 212 | - run: dart run grinder protobuf 213 | - name: Deploy 214 | run: dart run grinder pkg-github-release pkg-github-linux-ia32 pkg-github-linux-x64 215 | env: {GH_BEARER_TOKEN: "${{ github.token }}"} 216 | 217 | deploy_github_linux_qemu: 218 | name: "Deploy Github: linux-${{ matrix.arch }}" 219 | runs-on: ubuntu-latest 220 | strategy: 221 | matrix: 222 | include: 223 | - arch: arm 224 | platform: linux/arm/v7 225 | - arch: arm64 226 | platform: linux/arm64 227 | needs: [deploy_github_linux] 228 | if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass-embedded'" 229 | 230 | steps: 231 | - uses: actions/checkout@v3 232 | - uses: bufbuild/buf-setup-action@v1.13.1 233 | with: {github_token: "${{ github.token }}"} 234 | - uses: dart-lang/setup-dart@v1 235 | - run: dart pub get 236 | - run: dart run grinder protobuf 237 | - uses: docker/setup-qemu-action@v2 238 | - name: Deploy 239 | run: | 240 | docker run --rm \ 241 | --env "GH_BEARER_TOKEN=$GH_BEARER_TOKEN" \ 242 | --platform ${{ matrix.platform }} \ 243 | --volume "$PWD:$PWD" \ 244 | --workdir "$PWD" \ 245 | docker.io/library/dart:latest \ 246 | /bin/sh -c "dart pub get && dart run grinder pkg-github-linux-${{ matrix.arch }}" 247 | env: {GH_BEARER_TOKEN: "${{ github.token }}"} 248 | 249 | deploy_github: 250 | name: "Deploy Github: ${{ matrix.platform }}" 251 | runs-on: ${{ matrix.runner }} 252 | needs: [deploy_github_linux] 253 | if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass-embedded'" 254 | strategy: 255 | matrix: 256 | include: 257 | - runner: macos-latest 258 | platform: macos-x64 259 | architecture: x64 260 | - runner: self-hosted 261 | platform: macos-arm64 262 | architecture: arm64 263 | - runner: windows-latest 264 | platform: windows 265 | architecture: x64 266 | 267 | steps: 268 | - uses: actions/checkout@v3 269 | - uses: bufbuild/buf-setup-action@v1.13.1 270 | with: {github_token: "${{ github.token }}"} 271 | - uses: dart-lang/setup-dart@v1 272 | # Workaround for dart-lang/setup-dart#59 273 | with: 274 | architecture: ${{ matrix.architecture }} 275 | - run: dart pub get 276 | - run: dart run grinder protobuf 277 | - name: Deploy 278 | run: dart run grinder pkg-github-${{ matrix.platform }} 279 | env: {GH_BEARER_TOKEN: "${{ github.token }}"} 280 | 281 | deploy_homebrew: 282 | name: "Deploy Homebrew" 283 | runs-on: ubuntu-latest 284 | needs: [dart_tests, sass_spec, static_analysis, format] 285 | if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass-embedded'" 286 | 287 | steps: 288 | - uses: actions/checkout@v2 289 | - uses: dart-lang/setup-dart@v1 290 | - run: dart pub get 291 | - name: Deploy 292 | run: dart run grinder pkg-homebrew-update 293 | env: 294 | GH_TOKEN: "${{ secrets.GH_TOKEN }}" 295 | GH_USER: sassbot 296 | 297 | release_embedded_host: 298 | name: "Release Embedded Host" 299 | runs-on: ubuntu-latest 300 | needs: [deploy_github_linux, deploy_github_linux_qemu, deploy_github] 301 | if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass-embedded'" 302 | 303 | steps: 304 | - uses: actions/checkout@v3 305 | with: 306 | repository: sass/embedded-host-node 307 | token: ${{ secrets.GH_TOKEN }} 308 | 309 | - name: Get version 310 | id: version 311 | run: echo "::set-output name=version::${GITHUB_REF##*/}" 312 | 313 | - name: Update version 314 | run: | 315 | # Update binary package versions 316 | for dir in $(ls npm); do 317 | cat "npm/$dir/package.json" | 318 | jq --arg version ${{ steps.version.outputs.version }} ' 319 | .version |= $version 320 | ' > package.json.tmp && 321 | mv package.json.tmp "npm/$dir/package.json" 322 | done 323 | 324 | # Update main package version and dependencies on binary packages 325 | cat package.json | 326 | jq --arg version ${{ steps.version.outputs.version }} ' 327 | .version |= $version | 328 | ."compiler-version" |= $version | 329 | .optionalDependencies = (.optionalDependencies | .[] |= $version) 330 | ' > package.json.tmp && 331 | mv package.json.tmp package.json 332 | curl https://raw.githubusercontent.com/sass/dart-sass/${{ steps.version.outputs.version }}/CHANGELOG.md > CHANGELOG.md 333 | shell: bash 334 | 335 | - uses: EndBug/add-and-commit@v8 336 | with: 337 | author_name: Sass Bot 338 | author_email: sass.bot.beep.boop@gmail.com 339 | message: Update compiler version and release 340 | tag: ${{ steps.version.outputs.version }} 341 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/dart 2 | # Edit at https://www.gitignore.io/?templates=dart 3 | 4 | ### Dart ### 5 | # See https://www.dartlang.org/guides/libraries/private-files 6 | 7 | # Files and directories created by pub 8 | .dart_tool/ 9 | .packages 10 | build/ 11 | 12 | # Directory created by dartdoc 13 | # If you don't generate documentation locally you can remove this line. 14 | doc/api/ 15 | 16 | # Avoid committing generated Javascript files: 17 | *.dart.js 18 | *.info.json # Produced by the --dump-info flag. 19 | *.js # When generated by dart2js. Don't specify *.js if your 20 | # project includes source files written in JavaScript. 21 | *.js_ 22 | *.js.deps 23 | *.js.map 24 | 25 | # End of https://www.gitignore.io/api/dart 26 | 27 | # Generated protocol buffer files. 28 | *.pb*.dart 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Sass is more than a technology; Sass is driven by the community of individuals 2 | that power its development and use every day. As a community, we want to embrace 3 | the very differences that have made our collaboration so powerful, and work 4 | together to provide the best environment for learning, growing, and sharing of 5 | ideas. It is imperative that we keep Sass a fun, welcoming, challenging, and 6 | fair place to play. 7 | 8 | [The full community guidelines can be found on the Sass website.][link] 9 | 10 | [link]: http://sass-lang.com/community-guidelines 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Release process 26 | 27 | Because this package's version remains in lockstep with the current version of 28 | Dart Sass, it's not released manually from this repository. Instead, a release 29 | commit is automatically generated once a new Dart Sass version has been 30 | released. As such, manual commits should never: 31 | 32 | * Update the `pubspec.yaml`'s version to a non-`-dev` number. Changing it from 33 | non-`-dev` to dev when adding a new feature is fine. 34 | 35 | * Update the `pubspec.yaml`'s dependency on `sass` to a non-Git dependency. 36 | Changing it from non-Git to Git when using a new feature is fine. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Google LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This is no longer the repository for Embedded Dart Sass.** The embedded 2 | compiler has been merged into [the primary Dart Sass repository], and further 3 | releases will be included [as part of Dart Sass itself]. The embedded compiler 4 | can be accessed by running `sass --embedded`. 5 | 6 | [the primary Dart Sass repository]: https://github.com/sass/dart-sass 7 | [as part of Dart Sass itself]: https://github.com/sass/dart-sass#embedded-dart-sass 8 | 9 | ## Embedded Dart Sass 10 | 11 | This is a wrapper for [Dart Sass][] that implements the compiler side of the 12 | [Embedded Sass protocol][]. It's designed to be embedded in a host language, 13 | which then exposes an API for users to invoke Sass and define custom functions 14 | and importers. 15 | 16 | [Dart Sass]: https://sass-lang.com/dart-sass 17 | [Embedded Sass protocol]: https://github.com/sass/sass-embedded-protocol/blob/master/README.md#readme 18 | 19 | ### Usage 20 | 21 | - `dart-sass-embedded` starts the compiler and listens on stdin. 22 | - `dart-sass-embedded --version` prints `versionResponse` with `id = 0` in JSON and exits. 23 | 24 | ### Development 25 | 26 | To run the embedded compiler from source: 27 | 28 | * Run `dart pub get`. 29 | 30 | * [Install `buf`]. 31 | 32 | * Run `dart run grinder protobuf`. 33 | 34 | From there, you can either run `dart bin/dart_sass_embedded.dart` directly or 35 | `dart run grinder pkg-standalone-dev` to build a compiled development 36 | executable. 37 | 38 | [Install `buf`]: https://docs.buf.build/installation 39 | 40 | ### Releases 41 | 42 | Binary releases are available from the [GitHub release page]. We recommend that 43 | embedded hosts embed these release binaries in their packages, or use a 44 | post-install script to install a specific version of the embedded compiler to 45 | avoid version skew. 46 | 47 | [GitHub release page]: https://github.com/sass/dart-sass-embedded/releases 48 | 49 | Disclaimer: this is not an official Google product. 50 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:sass_analysis/analysis_options.yaml 2 | analyzer: 3 | exclude: ['**/*.pb*.dart'] 4 | -------------------------------------------------------------------------------- /bin/dart_sass_embedded.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:io'; 6 | import 'dart:convert'; 7 | 8 | import 'package:path/path.dart' as p; 9 | import 'package:sass/sass.dart' as sass; 10 | import 'package:stream_channel/stream_channel.dart'; 11 | 12 | import 'package:sass_embedded/src/dispatcher.dart'; 13 | import 'package:sass_embedded/src/embedded_sass.pb.dart'; 14 | import 'package:sass_embedded/src/function_registry.dart'; 15 | import 'package:sass_embedded/src/host_callable.dart'; 16 | import 'package:sass_embedded/src/importer/file.dart'; 17 | import 'package:sass_embedded/src/importer/host.dart'; 18 | import 'package:sass_embedded/src/logger.dart'; 19 | import 'package:sass_embedded/src/util/length_delimited_transformer.dart'; 20 | import 'package:sass_embedded/src/utils.dart'; 21 | 22 | void main(List args) { 23 | if (args.isNotEmpty) { 24 | if (args.first == "--version") { 25 | var response = Dispatcher.versionResponse(); 26 | response.id = 0; 27 | stdout.writeln( 28 | JsonEncoder.withIndent(" ").convert(response.toProto3Json())); 29 | return; 30 | } 31 | 32 | stderr.writeln( 33 | "This executable is not intended to be executed with arguments.\n" 34 | "See https://github.com/sass/embedded-protocol#readme for details."); 35 | // USAGE error from https://bit.ly/2poTt90 36 | exitCode = 64; 37 | return; 38 | } 39 | 40 | var dispatcher = Dispatcher( 41 | StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false) 42 | .transform(lengthDelimited)); 43 | 44 | dispatcher.listen((request) async { 45 | var functions = FunctionRegistry(); 46 | 47 | var style = request.style == OutputStyle.COMPRESSED 48 | ? sass.OutputStyle.compressed 49 | : sass.OutputStyle.expanded; 50 | var logger = Logger(dispatcher, request.id, 51 | color: request.alertColor, ascii: request.alertAscii); 52 | 53 | try { 54 | var importers = request.importers.map((importer) => 55 | _decodeImporter(dispatcher, request, importer) ?? 56 | (throw mandatoryError("Importer.importer"))); 57 | 58 | var globalFunctions = request.globalFunctions.map((signature) => 59 | hostCallable(dispatcher, functions, request.id, signature)); 60 | 61 | late sass.CompileResult result; 62 | switch (request.whichInput()) { 63 | case InboundMessage_CompileRequest_Input.string: 64 | var input = request.string; 65 | result = sass.compileStringToResult(input.source, 66 | color: request.alertColor, 67 | logger: logger, 68 | importers: importers, 69 | importer: _decodeImporter(dispatcher, request, input.importer) ?? 70 | (input.url.startsWith("file:") ? null : sass.Importer.noOp), 71 | functions: globalFunctions, 72 | syntax: syntaxToSyntax(input.syntax), 73 | style: style, 74 | url: input.url.isEmpty ? null : input.url, 75 | quietDeps: request.quietDeps, 76 | verbose: request.verbose, 77 | sourceMap: request.sourceMap, 78 | charset: request.charset); 79 | break; 80 | 81 | case InboundMessage_CompileRequest_Input.path: 82 | if (request.path.isEmpty) { 83 | throw mandatoryError("CompileRequest.Input.path"); 84 | } 85 | 86 | try { 87 | result = sass.compileToResult(request.path, 88 | color: request.alertColor, 89 | logger: logger, 90 | importers: importers, 91 | functions: globalFunctions, 92 | style: style, 93 | quietDeps: request.quietDeps, 94 | verbose: request.verbose, 95 | sourceMap: request.sourceMap, 96 | charset: request.charset); 97 | } on FileSystemException catch (error) { 98 | return OutboundMessage_CompileResponse() 99 | ..failure = (OutboundMessage_CompileResponse_CompileFailure() 100 | ..message = error.path == null 101 | ? error.message 102 | : "${error.message}: ${error.path}" 103 | ..span = (SourceSpan() 104 | ..start = SourceSpan_SourceLocation() 105 | ..end = SourceSpan_SourceLocation() 106 | ..url = p.toUri(request.path).toString())); 107 | } 108 | break; 109 | 110 | case InboundMessage_CompileRequest_Input.notSet: 111 | throw mandatoryError("CompileRequest.input"); 112 | } 113 | 114 | var success = OutboundMessage_CompileResponse_CompileSuccess() 115 | ..css = result.css 116 | ..loadedUrls.addAll(result.loadedUrls.map((url) => url.toString())); 117 | 118 | var sourceMap = result.sourceMap; 119 | if (sourceMap != null) { 120 | success.sourceMap = json.encode(sourceMap.toJson( 121 | includeSourceContents: request.sourceMapIncludeSources)); 122 | } 123 | return OutboundMessage_CompileResponse()..success = success; 124 | } on sass.SassException catch (error) { 125 | var formatted = withGlyphs( 126 | () => error.toString(color: request.alertColor), 127 | ascii: request.alertAscii); 128 | return OutboundMessage_CompileResponse() 129 | ..failure = (OutboundMessage_CompileResponse_CompileFailure() 130 | ..message = error.message 131 | ..span = protofySpan(error.span) 132 | ..stackTrace = error.trace.toString() 133 | ..formatted = formatted); 134 | } 135 | }); 136 | } 137 | 138 | /// Converts [importer] into a [sass.Importer]. 139 | sass.Importer? _decodeImporter( 140 | Dispatcher dispatcher, 141 | InboundMessage_CompileRequest request, 142 | InboundMessage_CompileRequest_Importer importer) { 143 | switch (importer.whichImporter()) { 144 | case InboundMessage_CompileRequest_Importer_Importer.path: 145 | return sass.FilesystemImporter(importer.path); 146 | 147 | case InboundMessage_CompileRequest_Importer_Importer.importerId: 148 | return HostImporter(dispatcher, request.id, importer.importerId); 149 | 150 | case InboundMessage_CompileRequest_Importer_Importer.fileImporterId: 151 | return FileImporter(dispatcher, request.id, importer.fileImporterId); 152 | 153 | case InboundMessage_CompileRequest_Importer_Importer.notSet: 154 | return null; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: dart 4 | out: lib/src 5 | -------------------------------------------------------------------------------- /buf.work.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | directories: [build/embedded-protocol] 3 | -------------------------------------------------------------------------------- /lib/src/dispatcher.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:async'; 6 | import 'dart:io'; 7 | import 'dart:typed_data'; 8 | 9 | import 'package:protobuf/protobuf.dart'; 10 | import 'package:stack_trace/stack_trace.dart'; 11 | import 'package:stream_channel/stream_channel.dart'; 12 | 13 | import 'embedded_sass.pb.dart'; 14 | import 'utils.dart'; 15 | 16 | /// A class that dispatches messages to and from the host. 17 | class Dispatcher { 18 | /// The channel of encoded protocol buffers, connected to the host. 19 | final StreamChannel _channel; 20 | 21 | /// Completers awaiting responses to outbound requests. 22 | /// 23 | /// The completers are located at indexes in this list matching the request 24 | /// IDs. `null` elements indicate IDs whose requests have been responded to, 25 | /// and which are therefore free to re-use. 26 | final _outstandingRequests = ?>[]; 27 | 28 | /// Creates a [Dispatcher] that sends and receives encoded protocol buffers 29 | /// over [channel]. 30 | Dispatcher(this._channel); 31 | 32 | /// Listens for incoming `CompileRequests` and passes them to [callback]. 33 | /// 34 | /// The callback must return a `CompileResponse` which is sent to the host. 35 | /// The callback may throw [ProtocolError]s, which will be sent back to the 36 | /// host. Neither `CompileResponse`s nor [ProtocolError]s need to set their 37 | /// `id` fields; the [Dispatcher] will take care of that. 38 | /// 39 | /// This may only be called once. 40 | void listen( 41 | FutureOr callback( 42 | InboundMessage_CompileRequest request)) { 43 | _channel.stream.listen((binaryMessage) async { 44 | // Wait a single microtask tick so that we're running in a separate 45 | // microtask from the initial request dispatch. Otherwise, [waitFor] will 46 | // deadlock the event loop fiber that would otherwise be checking stdin 47 | // for new input. 48 | await Future.value(); 49 | 50 | InboundMessage? message; 51 | try { 52 | try { 53 | message = InboundMessage.fromBuffer(binaryMessage); 54 | } on InvalidProtocolBufferException catch (error) { 55 | throw _parseError(error.message); 56 | } 57 | 58 | switch (message.whichMessage()) { 59 | case InboundMessage_Message.versionRequest: 60 | var request = message.versionRequest; 61 | var response = versionResponse(); 62 | response.id = request.id; 63 | _send(OutboundMessage()..versionResponse = response); 64 | break; 65 | 66 | case InboundMessage_Message.compileRequest: 67 | var request = message.compileRequest; 68 | var response = await callback(request); 69 | response.id = request.id; 70 | _send(OutboundMessage()..compileResponse = response); 71 | break; 72 | 73 | case InboundMessage_Message.canonicalizeResponse: 74 | var response = message.canonicalizeResponse; 75 | _dispatchResponse(response.id, response); 76 | break; 77 | 78 | case InboundMessage_Message.importResponse: 79 | var response = message.importResponse; 80 | _dispatchResponse(response.id, response); 81 | break; 82 | 83 | case InboundMessage_Message.fileImportResponse: 84 | var response = message.fileImportResponse; 85 | _dispatchResponse(response.id, response); 86 | break; 87 | 88 | case InboundMessage_Message.functionCallResponse: 89 | var response = message.functionCallResponse; 90 | _dispatchResponse(response.id, response); 91 | break; 92 | 93 | case InboundMessage_Message.notSet: 94 | throw _parseError("InboundMessage.message is not set."); 95 | 96 | default: 97 | throw _parseError( 98 | "Unknown message type: ${message.toDebugString()}"); 99 | } 100 | } on ProtocolError catch (error) { 101 | error.id = _inboundId(message) ?? errorId; 102 | stderr.write("Host caused ${error.type.name.toLowerCase()} error"); 103 | if (error.id != errorId) stderr.write(" with request ${error.id}"); 104 | stderr.writeln(": ${error.message}"); 105 | sendError(error); 106 | // PROTOCOL error from https://bit.ly/2poTt90 107 | exitCode = 76; 108 | _channel.sink.close(); 109 | } catch (error, stackTrace) { 110 | var errorMessage = "$error\n${Chain.forTrace(stackTrace)}"; 111 | stderr.write("Internal compiler error: $errorMessage"); 112 | sendError(ProtocolError() 113 | ..type = ProtocolErrorType.INTERNAL 114 | ..id = _inboundId(message) ?? errorId 115 | ..message = errorMessage); 116 | _channel.sink.close(); 117 | } 118 | }); 119 | } 120 | 121 | /// Sends [event] to the host. 122 | void sendLog(OutboundMessage_LogEvent event) => 123 | _send(OutboundMessage()..logEvent = event); 124 | 125 | /// Sends [error] to the host. 126 | void sendError(ProtocolError error) => 127 | _send(OutboundMessage()..error = error); 128 | 129 | Future sendCanonicalizeRequest( 130 | OutboundMessage_CanonicalizeRequest request) => 131 | _sendRequest( 132 | OutboundMessage()..canonicalizeRequest = request); 133 | 134 | Future sendImportRequest( 135 | OutboundMessage_ImportRequest request) => 136 | _sendRequest( 137 | OutboundMessage()..importRequest = request); 138 | 139 | Future sendFileImportRequest( 140 | OutboundMessage_FileImportRequest request) => 141 | _sendRequest( 142 | OutboundMessage()..fileImportRequest = request); 143 | 144 | Future sendFunctionCallRequest( 145 | OutboundMessage_FunctionCallRequest request) => 146 | _sendRequest( 147 | OutboundMessage()..functionCallRequest = request); 148 | 149 | /// Sends [request] to the host and returns the message sent in response. 150 | Future _sendRequest( 151 | OutboundMessage request) async { 152 | var id = _nextRequestId(); 153 | _setOutboundId(request, id); 154 | _send(request); 155 | 156 | var completer = Completer(); 157 | _outstandingRequests[id] = completer; 158 | return completer.future; 159 | } 160 | 161 | /// Returns an available request ID, and guarantees that its slot is available 162 | /// in [_outstandingRequests]. 163 | int _nextRequestId() { 164 | for (var i = 0; i < _outstandingRequests.length; i++) { 165 | if (_outstandingRequests[i] == null) return i; 166 | } 167 | 168 | // If there are no empty slots, add another one. 169 | _outstandingRequests.add(null); 170 | return _outstandingRequests.length - 1; 171 | } 172 | 173 | /// Dispatches [response] to the appropriate outstanding request. 174 | /// 175 | /// Throws an error if there's no outstanding request with the given [id] or 176 | /// if that request is expecting a different type of response. 177 | void _dispatchResponse(int id, T response) { 178 | Completer? completer; 179 | if (id < _outstandingRequests.length) { 180 | completer = _outstandingRequests[id]; 181 | _outstandingRequests[id] = null; 182 | } 183 | 184 | if (completer == null) { 185 | throw paramsError( 186 | "Response ID $id doesn't match any outstanding requests."); 187 | } else if (completer is! Completer) { 188 | throw paramsError("Request ID $id doesn't match response type " 189 | "${response.runtimeType}."); 190 | } 191 | 192 | completer.complete(response); 193 | } 194 | 195 | /// Sends [message] to the host. 196 | void _send(OutboundMessage message) => 197 | _channel.sink.add(message.writeToBuffer()); 198 | 199 | /// Returns a [ProtocolError] with type `PARSE` and the given [message]. 200 | ProtocolError _parseError(String message) => ProtocolError() 201 | ..type = ProtocolErrorType.PARSE 202 | ..message = message; 203 | 204 | /// Returns the id for [message] if it it's a request, or `null` 205 | /// otherwise. 206 | int? _inboundId(InboundMessage? message) { 207 | if (message == null) return null; 208 | switch (message.whichMessage()) { 209 | case InboundMessage_Message.compileRequest: 210 | return message.compileRequest.id; 211 | default: 212 | return null; 213 | } 214 | } 215 | 216 | /// Sets the id for [message] to [id]. 217 | /// 218 | /// Throws an [ArgumentError] if [message] doesn't have an id field. 219 | void _setOutboundId(OutboundMessage message, int id) { 220 | switch (message.whichMessage()) { 221 | case OutboundMessage_Message.compileResponse: 222 | message.compileResponse.id = id; 223 | break; 224 | case OutboundMessage_Message.canonicalizeRequest: 225 | message.canonicalizeRequest.id = id; 226 | break; 227 | case OutboundMessage_Message.importRequest: 228 | message.importRequest.id = id; 229 | break; 230 | case OutboundMessage_Message.fileImportRequest: 231 | message.fileImportRequest.id = id; 232 | break; 233 | case OutboundMessage_Message.functionCallRequest: 234 | message.functionCallRequest.id = id; 235 | break; 236 | case OutboundMessage_Message.versionResponse: 237 | message.versionResponse.id = id; 238 | break; 239 | default: 240 | throw ArgumentError("Unknown message type: ${message.toDebugString()}"); 241 | } 242 | } 243 | 244 | /// Creates a [OutboundMessage_VersionResponse] 245 | static OutboundMessage_VersionResponse versionResponse() { 246 | return OutboundMessage_VersionResponse() 247 | ..protocolVersion = const String.fromEnvironment("protocol-version") 248 | ..compilerVersion = const String.fromEnvironment("compiler-version") 249 | ..implementationVersion = 250 | const String.fromEnvironment("implementation-version") 251 | ..implementationName = "Dart Sass"; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /lib/src/function_registry.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_api/sass_api.dart' as sass; 6 | 7 | import 'embedded_sass.pb.dart'; 8 | 9 | /// A registry of [SassFunction]s indexed by ID so that the host can invoke 10 | /// them. 11 | class FunctionRegistry { 12 | /// First-class functions that have been sent to the host. 13 | /// 14 | /// The functions are located at indexes in the list matching their IDs. 15 | final _functionsById = []; 16 | 17 | /// A reverse map from functions to their indexes in [_functionsById]. 18 | final _idsByFunction = {}; 19 | 20 | /// Converts [function] to a protocol buffer to send to the host. 21 | Value_CompilerFunction protofy(sass.SassFunction function) { 22 | var id = _idsByFunction.putIfAbsent(function, () { 23 | _functionsById.add(function); 24 | return _functionsById.length - 1; 25 | }); 26 | 27 | return Value_CompilerFunction()..id = id; 28 | } 29 | 30 | /// Returns the compiler-side function associated with [id]. 31 | /// 32 | /// If no such function exists, returns `null`. 33 | sass.SassFunction? operator [](int id) => _functionsById[id]; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/host_callable.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // ignore: deprecated_member_use 6 | import 'dart:cli'; 7 | import 'dart:io'; 8 | 9 | import 'package:sass_api/sass_api.dart' as sass; 10 | 11 | import 'dispatcher.dart'; 12 | import 'embedded_sass.pb.dart'; 13 | import 'function_registry.dart'; 14 | import 'protofier.dart'; 15 | import 'utils.dart'; 16 | 17 | /// Returns a Sass callable that invokes a function defined on the host with the 18 | /// given [signature]. 19 | /// 20 | /// If [id] is passed, the function will be called by ID (which is necessary for 21 | /// anonymous functions defined on the host). Otherwise, it will be called using 22 | /// the name defined in the [signature]. 23 | /// 24 | /// Throws a [sass.SassException] if [signature] is invalid. 25 | sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions, 26 | int compilationId, String signature, 27 | {int? id}) { 28 | late sass.Callable callable; 29 | callable = sass.Callable.fromSignature(signature, (arguments) { 30 | var protofier = Protofier(dispatcher, functions, compilationId); 31 | var request = OutboundMessage_FunctionCallRequest() 32 | ..compilationId = compilationId 33 | ..arguments.addAll( 34 | [for (var argument in arguments) protofier.protofy(argument)]); 35 | 36 | if (id != null) { 37 | request.functionId = id; 38 | } else { 39 | request.name = callable.name; 40 | } 41 | 42 | // ignore: deprecated_member_use 43 | var response = waitFor(dispatcher.sendFunctionCallRequest(request)); 44 | try { 45 | switch (response.whichResult()) { 46 | case InboundMessage_FunctionCallResponse_Result.success: 47 | return protofier.deprotofyResponse(response); 48 | 49 | case InboundMessage_FunctionCallResponse_Result.error: 50 | throw response.error; 51 | 52 | case InboundMessage_FunctionCallResponse_Result.notSet: 53 | throw mandatoryError('FunctionCallResponse.result'); 54 | } 55 | } on ProtocolError catch (error) { 56 | error.id = errorId; 57 | stderr.writeln("Host caused ${error.type.name.toLowerCase()} error: " 58 | "${error.message}"); 59 | dispatcher.sendError(error); 60 | throw error.message; 61 | } 62 | }); 63 | return callable; 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/importer/base.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:meta/meta.dart'; 6 | import 'package:sass_api/sass_api.dart' as sass; 7 | 8 | import '../dispatcher.dart'; 9 | 10 | /// An abstract base class for importers that communicate with the host in some 11 | /// way. 12 | abstract class ImporterBase extends sass.Importer { 13 | /// The [Dispatcher] to which to send requests. 14 | @protected 15 | final Dispatcher dispatcher; 16 | 17 | ImporterBase(this.dispatcher); 18 | 19 | /// Parses [url] as a [Uri] and throws an error if it's invalid or relative 20 | /// (including root-relative). 21 | /// 22 | /// The [source] name is used in the error message if one is thrown. 23 | @protected 24 | Uri parseAbsoluteUrl(String source, String url) { 25 | Uri parsedUrl; 26 | try { 27 | parsedUrl = Uri.parse(url); 28 | } on FormatException { 29 | throw '$source must return a URL, was "$url"'; 30 | } 31 | 32 | if (parsedUrl.scheme.isNotEmpty) return parsedUrl; 33 | throw '$source must return an absolute URL, was "$parsedUrl"'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/importer/file.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // ignore: deprecated_member_use 6 | import 'dart:cli'; 7 | 8 | import 'package:sass_api/sass_api.dart' as sass; 9 | 10 | import '../dispatcher.dart'; 11 | import '../embedded_sass.pb.dart' hide SourceSpan; 12 | import 'base.dart'; 13 | 14 | /// A filesystem importer to use for most implementation details of 15 | /// [FileImporter]. 16 | /// 17 | /// This allows us to avoid duplicating logic between the two importers. 18 | final _filesystemImporter = sass.FilesystemImporter('.'); 19 | 20 | /// An importer that asks the host to resolve imports in a simplified, 21 | /// file-system-centric way. 22 | class FileImporter extends ImporterBase { 23 | /// The ID of the compilation in which this importer is used. 24 | final int _compilationId; 25 | 26 | /// The host-provided ID of the importer to invoke. 27 | final int _importerId; 28 | 29 | FileImporter(Dispatcher dispatcher, this._compilationId, this._importerId) 30 | : super(dispatcher); 31 | 32 | Uri? canonicalize(Uri url) { 33 | if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); 34 | 35 | // ignore: deprecated_member_use 36 | return waitFor(() async { 37 | var response = await dispatcher 38 | .sendFileImportRequest(OutboundMessage_FileImportRequest() 39 | ..compilationId = _compilationId 40 | ..importerId = _importerId 41 | ..url = url.toString() 42 | ..fromImport = fromImport); 43 | 44 | switch (response.whichResult()) { 45 | case InboundMessage_FileImportResponse_Result.fileUrl: 46 | var url = parseAbsoluteUrl("The file importer", response.fileUrl); 47 | if (url.scheme != 'file') { 48 | throw 'The file importer must return a file: URL, was "$url"'; 49 | } 50 | 51 | return _filesystemImporter.canonicalize(url); 52 | 53 | case InboundMessage_FileImportResponse_Result.error: 54 | throw response.error; 55 | 56 | case InboundMessage_FileImportResponse_Result.notSet: 57 | return null; 58 | } 59 | }()); 60 | } 61 | 62 | sass.ImporterResult? load(Uri url) => _filesystemImporter.load(url); 63 | 64 | String toString() => "FileImporter"; 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/importer/host.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | // ignore: deprecated_member_use 6 | import 'dart:cli'; 7 | 8 | import 'package:sass_api/sass_api.dart' as sass; 9 | 10 | import '../dispatcher.dart'; 11 | import '../embedded_sass.pb.dart' hide SourceSpan; 12 | import '../utils.dart'; 13 | import 'base.dart'; 14 | 15 | /// An importer that asks the host to resolve imports. 16 | class HostImporter extends ImporterBase { 17 | /// The ID of the compilation in which this importer is used. 18 | final int _compilationId; 19 | 20 | /// The host-provided ID of the importer to invoke. 21 | final int _importerId; 22 | 23 | HostImporter(Dispatcher dispatcher, this._compilationId, this._importerId) 24 | : super(dispatcher); 25 | 26 | Uri? canonicalize(Uri url) { 27 | // ignore: deprecated_member_use 28 | return waitFor(() async { 29 | var response = await dispatcher 30 | .sendCanonicalizeRequest(OutboundMessage_CanonicalizeRequest() 31 | ..compilationId = _compilationId 32 | ..importerId = _importerId 33 | ..url = url.toString() 34 | ..fromImport = fromImport); 35 | 36 | switch (response.whichResult()) { 37 | case InboundMessage_CanonicalizeResponse_Result.url: 38 | return parseAbsoluteUrl("The importer", response.url); 39 | 40 | case InboundMessage_CanonicalizeResponse_Result.error: 41 | throw response.error; 42 | 43 | case InboundMessage_CanonicalizeResponse_Result.notSet: 44 | return null; 45 | } 46 | }()); 47 | } 48 | 49 | sass.ImporterResult? load(Uri url) { 50 | // ignore: deprecated_member_use 51 | return waitFor(() async { 52 | var response = 53 | await dispatcher.sendImportRequest(OutboundMessage_ImportRequest() 54 | ..compilationId = _compilationId 55 | ..importerId = _importerId 56 | ..url = url.toString()); 57 | 58 | switch (response.whichResult()) { 59 | case InboundMessage_ImportResponse_Result.success: 60 | return sass.ImporterResult(response.success.contents, 61 | sourceMapUrl: response.success.sourceMapUrl.isEmpty 62 | ? null 63 | : parseAbsoluteUrl( 64 | "The importer", response.success.sourceMapUrl), 65 | syntax: syntaxToSyntax(response.success.syntax)); 66 | 67 | case InboundMessage_ImportResponse_Result.error: 68 | throw response.error; 69 | 70 | case InboundMessage_ImportResponse_Result.notSet: 71 | return null; 72 | } 73 | }()); 74 | } 75 | 76 | String toString() => "HostImporter"; 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/logger.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:path/path.dart' as p; 6 | import 'package:sass_api/sass_api.dart' as sass; 7 | import 'package:source_span/source_span.dart'; 8 | import 'package:stack_trace/stack_trace.dart'; 9 | 10 | import 'dispatcher.dart'; 11 | import 'embedded_sass.pb.dart' hide SourceSpan; 12 | import 'utils.dart'; 13 | 14 | /// A Sass logger that sends log messages as `LogEvent`s. 15 | class Logger implements sass.Logger { 16 | /// The [Dispatcher] to which to send events. 17 | final Dispatcher _dispatcher; 18 | 19 | /// The ID of the compilation to which this logger is passed. 20 | final int _compilationId; 21 | 22 | /// Whether the formatted message should contain terminal colors. 23 | final bool _color; 24 | 25 | /// Whether the formatted message should use ASCII encoding. 26 | final bool _ascii; 27 | 28 | Logger(this._dispatcher, this._compilationId, 29 | {bool color = false, bool ascii = false}) 30 | : _color = color, 31 | _ascii = ascii; 32 | 33 | void debug(String message, SourceSpan span) { 34 | var url = 35 | span.start.sourceUrl == null ? '-' : p.prettyUri(span.start.sourceUrl); 36 | var buffer = StringBuffer() 37 | ..write('$url:${span.start.line + 1} ') 38 | ..write(_color ? '\u001b[1mDebug\u001b[0m' : 'DEBUG') 39 | ..writeln(': $message'); 40 | 41 | _dispatcher.sendLog(OutboundMessage_LogEvent() 42 | ..compilationId = _compilationId 43 | ..type = LogEventType.DEBUG 44 | ..message = message 45 | ..span = protofySpan(span) 46 | ..formatted = buffer.toString()); 47 | } 48 | 49 | void warn(String message, 50 | {FileSpan? span, Trace? trace, bool deprecation = false}) { 51 | var formatted = withGlyphs(() { 52 | var buffer = StringBuffer(); 53 | if (_color) { 54 | buffer.write('\u001b[33m\u001b[1m'); 55 | if (deprecation) buffer.write('Deprecation '); 56 | buffer.write('Warning\u001b[0m'); 57 | } else { 58 | if (deprecation) buffer.write('DEPRECATION '); 59 | buffer.write('WARNING'); 60 | } 61 | if (span == null) { 62 | buffer.writeln(': $message'); 63 | } else if (trace != null) { 64 | buffer.writeln(': $message\n\n${span.highlight(color: _color)}'); 65 | } else { 66 | buffer.writeln(' on ${span.message("\n" + message, color: _color)}'); 67 | } 68 | if (trace != null) { 69 | buffer.writeln(indent(trace.toString().trimRight(), 4)); 70 | } 71 | return buffer.toString(); 72 | }, ascii: _ascii); 73 | 74 | var event = OutboundMessage_LogEvent() 75 | ..compilationId = _compilationId 76 | ..type = 77 | deprecation ? LogEventType.DEPRECATION_WARNING : LogEventType.WARNING 78 | ..message = message 79 | ..formatted = formatted; 80 | if (span != null) event.span = protofySpan(span); 81 | if (trace != null) event.stackTrace = trace.toString(); 82 | _dispatcher.sendLog(event); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/protofier.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_api/sass_api.dart' as sass; 6 | 7 | import 'dispatcher.dart'; 8 | import 'embedded_sass.pb.dart'; 9 | import 'function_registry.dart'; 10 | import 'host_callable.dart'; 11 | import 'utils.dart'; 12 | 13 | /// A class that converts Sass [sass.Value] objects into [Value] protobufs. 14 | /// 15 | /// A given [Protofier] instance is valid only within the scope of a single 16 | /// custom function call. 17 | class Protofier { 18 | /// The dispatcher, for invoking deprotofied [Value_HostFunction]s. 19 | final Dispatcher _dispatcher; 20 | 21 | /// The IDs of first-class functions. 22 | final FunctionRegistry _functions; 23 | 24 | /// The ID of the current compilation. 25 | final int _compilationId; 26 | 27 | /// Any argument lists transitively contained in [value]. 28 | /// 29 | /// The IDs of the [Value_ArgumentList] protobufs are always one greater than 30 | /// the index of the corresponding list in this array (since 0 is reserved for 31 | /// argument lists created by the host). 32 | final _argumentLists = []; 33 | 34 | /// Creates a [Protofier] that's valid within the scope of a single custom 35 | /// function call. 36 | /// 37 | /// The [functions] tracks the IDs of first-class functions so that the host 38 | /// can pass them back to the compiler. 39 | Protofier(this._dispatcher, this._functions, this._compilationId); 40 | 41 | /// Converts [value] to its protocol buffer representation. 42 | Value protofy(sass.Value value) { 43 | var result = Value(); 44 | if (value is sass.SassString) { 45 | result.string = Value_String() 46 | ..text = value.text 47 | ..quoted = value.hasQuotes; 48 | } else if (value is sass.SassNumber) { 49 | result.number = _protofyNumber(value); 50 | } else if (value is sass.SassColor) { 51 | if (value.hasCalculatedHsl) { 52 | result.hslColor = Value_HslColor() 53 | ..hue = value.hue * 1.0 54 | ..saturation = value.saturation * 1.0 55 | ..lightness = value.lightness * 1.0 56 | ..alpha = value.alpha * 1.0; 57 | } else { 58 | result.rgbColor = Value_RgbColor() 59 | ..red = value.red 60 | ..green = value.green 61 | ..blue = value.blue 62 | ..alpha = value.alpha * 1.0; 63 | } 64 | } else if (value is sass.SassArgumentList) { 65 | _argumentLists.add(value); 66 | var argList = Value_ArgumentList() 67 | ..id = _argumentLists.length 68 | ..separator = _protofySeparator(value.separator) 69 | ..contents.addAll([for (var element in value.asList) protofy(element)]); 70 | value.keywordsWithoutMarking.forEach((key, value) { 71 | argList.keywords[key] = protofy(value); 72 | }); 73 | 74 | result.argumentList = argList; 75 | } else if (value is sass.SassList) { 76 | result.list = Value_List() 77 | ..separator = _protofySeparator(value.separator) 78 | ..hasBrackets = value.hasBrackets 79 | ..contents.addAll([for (var element in value.asList) protofy(element)]); 80 | } else if (value is sass.SassMap) { 81 | var map = Value_Map(); 82 | value.contents.forEach((key, value) { 83 | map.entries.add(Value_Map_Entry() 84 | ..key = protofy(key) 85 | ..value = protofy(value)); 86 | }); 87 | result.map = map; 88 | } else if (value is sass.SassCalculation) { 89 | result.calculation = _protofyCalculation(value); 90 | } else if (value is sass.SassFunction) { 91 | result.compilerFunction = _functions.protofy(value); 92 | } else if (value == sass.sassTrue) { 93 | result.singleton = SingletonValue.TRUE; 94 | } else if (value == sass.sassFalse) { 95 | result.singleton = SingletonValue.FALSE; 96 | } else if (value == sass.sassNull) { 97 | result.singleton = SingletonValue.NULL; 98 | } else { 99 | throw "Unknown Value $value"; 100 | } 101 | return result; 102 | } 103 | 104 | /// Converts [number] to its protocol buffer representation. 105 | Value_Number _protofyNumber(sass.SassNumber number) { 106 | var value = Value_Number()..value = number.value * 1.0; 107 | value.numerators.addAll(number.numeratorUnits); 108 | value.denominators.addAll(number.denominatorUnits); 109 | return value; 110 | } 111 | 112 | /// Converts [separator] to its protocol buffer representation. 113 | ListSeparator _protofySeparator(sass.ListSeparator separator) { 114 | switch (separator) { 115 | case sass.ListSeparator.comma: 116 | return ListSeparator.COMMA; 117 | case sass.ListSeparator.space: 118 | return ListSeparator.SPACE; 119 | case sass.ListSeparator.slash: 120 | return ListSeparator.SLASH; 121 | case sass.ListSeparator.undecided: 122 | return ListSeparator.UNDECIDED; 123 | default: 124 | throw "Unknown ListSeparator $separator"; 125 | } 126 | } 127 | 128 | /// Converts [calculation] to its protocol buffer representation. 129 | Value_Calculation _protofyCalculation(sass.SassCalculation calculation) => 130 | Value_Calculation() 131 | ..name = calculation.name 132 | ..arguments.addAll([ 133 | for (var argument in calculation.arguments) 134 | _protofyCalculationValue(argument) 135 | ]); 136 | 137 | /// Converts a calculation value that appears within a `SassCalculation` to 138 | /// its protocol buffer representation. 139 | Value_Calculation_CalculationValue _protofyCalculationValue(Object value) { 140 | var result = Value_Calculation_CalculationValue(); 141 | if (value is sass.SassNumber) { 142 | result.number = _protofyNumber(value); 143 | } else if (value is sass.SassCalculation) { 144 | result.calculation = _protofyCalculation(value); 145 | } else if (value is sass.SassString) { 146 | result.string = value.text; 147 | } else if (value is sass.CalculationOperation) { 148 | result.operation = Value_Calculation_CalculationOperation() 149 | ..operator = _protofyCalculationOperator(value.operator) 150 | ..left = _protofyCalculationValue(value.left) 151 | ..right = _protofyCalculationValue(value.right); 152 | } else if (value is sass.CalculationInterpolation) { 153 | result.interpolation = value.value; 154 | } else { 155 | throw "Unknown calculation value $value"; 156 | } 157 | return result; 158 | } 159 | 160 | /// Converts [operator] to its protocol buffer representation. 161 | CalculationOperator _protofyCalculationOperator( 162 | sass.CalculationOperator operator) { 163 | switch (operator) { 164 | case sass.CalculationOperator.plus: 165 | return CalculationOperator.PLUS; 166 | case sass.CalculationOperator.minus: 167 | return CalculationOperator.MINUS; 168 | case sass.CalculationOperator.times: 169 | return CalculationOperator.TIMES; 170 | case sass.CalculationOperator.dividedBy: 171 | return CalculationOperator.DIVIDE; 172 | default: 173 | throw "Unknown CalculationOperator $operator"; 174 | } 175 | } 176 | 177 | /// Converts [response]'s return value to its Sass representation. 178 | sass.Value deprotofyResponse(InboundMessage_FunctionCallResponse response) { 179 | for (var id in response.accessedArgumentLists) { 180 | // Mark the `keywords` field as accessed. 181 | _argumentListForId(id).keywords; 182 | } 183 | 184 | return _deprotofy(response.success); 185 | } 186 | 187 | /// Converts [value] to its Sass representation. 188 | sass.Value _deprotofy(Value value) { 189 | try { 190 | switch (value.whichValue()) { 191 | case Value_Value.string: 192 | return value.string.text.isEmpty 193 | ? sass.SassString.empty(quotes: value.string.quoted) 194 | : sass.SassString(value.string.text, quotes: value.string.quoted); 195 | 196 | case Value_Value.number: 197 | return _deprotofyNumber(value.number); 198 | 199 | case Value_Value.rgbColor: 200 | return sass.SassColor.rgb(value.rgbColor.red, value.rgbColor.green, 201 | value.rgbColor.blue, value.rgbColor.alpha); 202 | 203 | case Value_Value.hslColor: 204 | return sass.SassColor.hsl( 205 | value.hslColor.hue, 206 | value.hslColor.saturation, 207 | value.hslColor.lightness, 208 | value.hslColor.alpha); 209 | 210 | case Value_Value.hwbColor: 211 | return sass.SassColor.hwb( 212 | value.hwbColor.hue, 213 | value.hwbColor.whiteness, 214 | value.hwbColor.blackness, 215 | value.hwbColor.alpha); 216 | 217 | case Value_Value.argumentList: 218 | if (value.argumentList.id != 0) { 219 | return _argumentListForId(value.argumentList.id); 220 | } 221 | 222 | var separator = _deprotofySeparator(value.argumentList.separator); 223 | var length = value.argumentList.contents.length; 224 | if (separator == sass.ListSeparator.undecided && length > 1) { 225 | throw paramsError( 226 | "List $value can't have an undecided separator because it has " 227 | "$length elements"); 228 | } 229 | 230 | return sass.SassArgumentList([ 231 | for (var element in value.argumentList.contents) _deprotofy(element) 232 | ], { 233 | for (var entry in value.argumentList.keywords.entries) 234 | entry.key: _deprotofy(entry.value) 235 | }, separator); 236 | 237 | case Value_Value.list: 238 | var separator = _deprotofySeparator(value.list.separator); 239 | if (value.list.contents.isEmpty) { 240 | return sass.SassList.empty( 241 | separator: separator, brackets: value.list.hasBrackets); 242 | } 243 | 244 | var length = value.list.contents.length; 245 | if (separator == sass.ListSeparator.undecided && length > 1) { 246 | throw paramsError( 247 | "List $value can't have an undecided separator because it has " 248 | "$length elements"); 249 | } 250 | 251 | return sass.SassList([ 252 | for (var element in value.list.contents) _deprotofy(element) 253 | ], separator, brackets: value.list.hasBrackets); 254 | 255 | case Value_Value.map: 256 | return value.map.entries.isEmpty 257 | ? const sass.SassMap.empty() 258 | : sass.SassMap({ 259 | for (var entry in value.map.entries) 260 | _deprotofy(entry.key): _deprotofy(entry.value) 261 | }); 262 | 263 | case Value_Value.compilerFunction: 264 | var id = value.compilerFunction.id; 265 | var function = _functions[id]; 266 | if (function == null) { 267 | throw paramsError( 268 | "CompilerFunction.id $id doesn't match any known functions"); 269 | } 270 | 271 | return function; 272 | 273 | case Value_Value.hostFunction: 274 | return sass.SassFunction(hostCallable(_dispatcher, _functions, 275 | _compilationId, value.hostFunction.signature, 276 | id: value.hostFunction.id)); 277 | 278 | case Value_Value.calculation: 279 | return _deprotofyCalculation(value.calculation); 280 | 281 | case Value_Value.singleton: 282 | switch (value.singleton) { 283 | case SingletonValue.TRUE: 284 | return sass.sassTrue; 285 | case SingletonValue.FALSE: 286 | return sass.sassFalse; 287 | case SingletonValue.NULL: 288 | return sass.sassNull; 289 | default: 290 | throw "Unknown Value.singleton ${value.singleton}"; 291 | } 292 | 293 | case Value_Value.notSet: 294 | throw mandatoryError("Value.value"); 295 | } 296 | } on RangeError catch (error) { 297 | var name = error.name; 298 | if (name == null || error.start == null || error.end == null) { 299 | throw paramsError(error.toString()); 300 | } 301 | 302 | if (value.whichValue() == Value_Value.rgbColor) { 303 | name = 'RgbColor.$name'; 304 | } else if (value.whichValue() == Value_Value.hslColor) { 305 | name = 'HslColor.$name'; 306 | } 307 | 308 | throw paramsError( 309 | '$name must be between ${error.start} and ${error.end}, was ' 310 | '${error.invalidValue}'); 311 | } 312 | } 313 | 314 | /// Converts [number] to its Sass representation. 315 | sass.SassNumber _deprotofyNumber(Value_Number number) => 316 | sass.SassNumber.withUnits(number.value, 317 | numeratorUnits: number.numerators, 318 | denominatorUnits: number.denominators); 319 | 320 | /// Returns the argument list in [_argumentLists] that corresponds to [id]. 321 | sass.SassArgumentList _argumentListForId(int id) { 322 | if (id < 1) { 323 | throw paramsError( 324 | "Value.ArgumentList.id $id can't be marked as accessed"); 325 | } else if (id > _argumentLists.length) { 326 | throw paramsError( 327 | "Value.ArgumentList.id $id doesn't match any known argument " 328 | "lists"); 329 | } else { 330 | return _argumentLists[id - 1]; 331 | } 332 | } 333 | 334 | /// Converts [separator] to its Sass representation. 335 | sass.ListSeparator _deprotofySeparator(ListSeparator separator) { 336 | switch (separator) { 337 | case ListSeparator.COMMA: 338 | return sass.ListSeparator.comma; 339 | case ListSeparator.SPACE: 340 | return sass.ListSeparator.space; 341 | case ListSeparator.SLASH: 342 | return sass.ListSeparator.slash; 343 | case ListSeparator.UNDECIDED: 344 | return sass.ListSeparator.undecided; 345 | default: 346 | throw "Unknown separator $separator"; 347 | } 348 | } 349 | 350 | /// Converts [calculation] to its Sass representation. 351 | sass.Value _deprotofyCalculation(Value_Calculation calculation) { 352 | if (calculation.name == "calc") { 353 | if (calculation.arguments.length != 1) { 354 | throw paramsError( 355 | "Value.Calculation.arguments must have exactly one argument for " 356 | "calc()."); 357 | } 358 | 359 | return sass.SassCalculation.calc( 360 | _deprotofyCalculationValue(calculation.arguments[0])); 361 | } else if (calculation.name == "clamp") { 362 | if (calculation.arguments.length != 3) { 363 | throw paramsError( 364 | "Value.Calculation.arguments must have exactly 3 arguments for " 365 | "clamp()."); 366 | } 367 | 368 | return sass.SassCalculation.clamp( 369 | _deprotofyCalculationValue(calculation.arguments[0]), 370 | _deprotofyCalculationValue(calculation.arguments[1]), 371 | _deprotofyCalculationValue(calculation.arguments[2])); 372 | } else if (calculation.name == "min") { 373 | if (calculation.arguments.isEmpty) { 374 | throw paramsError( 375 | "Value.Calculation.arguments must have at least 1 argument for " 376 | "min()."); 377 | } 378 | 379 | return sass.SassCalculation.min( 380 | calculation.arguments.map(_deprotofyCalculationValue)); 381 | } else if (calculation.name == "max") { 382 | if (calculation.arguments.isEmpty) { 383 | throw paramsError( 384 | "Value.Calculation.arguments must have at least 1 argument for " 385 | "max()."); 386 | } 387 | 388 | return sass.SassCalculation.max( 389 | calculation.arguments.map(_deprotofyCalculationValue)); 390 | } else { 391 | throw paramsError( 392 | 'Value.Calculation.name "${calculation.name}" is not a recognized ' 393 | 'calculation type.'); 394 | } 395 | } 396 | 397 | /// Converts [value] to its Sass representation. 398 | Object _deprotofyCalculationValue(Value_Calculation_CalculationValue value) { 399 | switch (value.whichValue()) { 400 | case Value_Calculation_CalculationValue_Value.number: 401 | return _deprotofyNumber(value.number); 402 | 403 | case Value_Calculation_CalculationValue_Value.calculation: 404 | return _deprotofyCalculation(value.calculation); 405 | 406 | case Value_Calculation_CalculationValue_Value.string: 407 | return sass.SassString(value.string, quotes: false); 408 | 409 | case Value_Calculation_CalculationValue_Value.operation: 410 | return sass.SassCalculation.operate( 411 | _deprotofyCalculationOperator(value.operation.operator), 412 | _deprotofyCalculationValue(value.operation.left), 413 | _deprotofyCalculationValue(value.operation.right)); 414 | 415 | case Value_Calculation_CalculationValue_Value.interpolation: 416 | return sass.CalculationInterpolation(value.interpolation); 417 | 418 | case Value_Calculation_CalculationValue_Value.notSet: 419 | throw mandatoryError("Value.Calculation.value"); 420 | } 421 | } 422 | 423 | /// Converts [operator] to its Sass representation. 424 | sass.CalculationOperator _deprotofyCalculationOperator( 425 | CalculationOperator operator) { 426 | switch (operator) { 427 | case CalculationOperator.PLUS: 428 | return sass.CalculationOperator.plus; 429 | case CalculationOperator.MINUS: 430 | return sass.CalculationOperator.minus; 431 | case CalculationOperator.TIMES: 432 | return sass.CalculationOperator.times; 433 | case CalculationOperator.DIVIDE: 434 | return sass.CalculationOperator.dividedBy; 435 | default: 436 | throw "Unknown CalculationOperator $operator"; 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /lib/src/util/length_delimited_transformer.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:async'; 6 | import 'dart:math' as math; 7 | import 'dart:typed_data'; 8 | 9 | import 'package:async/async.dart'; 10 | import 'package:stream_channel/stream_channel.dart'; 11 | import 'package:typed_data/typed_data.dart'; 12 | 13 | /// A [StreamChannelTransformer] that converts a channel that sends and receives 14 | /// arbitrarily-chunked binary data to one that sends and receives packets of 15 | /// set length using [lengthDelimitedEncoder] and [lengthDelimitedDecoder]. 16 | final StreamChannelTransformer> lengthDelimited = 17 | StreamChannelTransformer>(lengthDelimitedDecoder, 18 | StreamSinkTransformer.fromStreamTransformer(lengthDelimitedEncoder)); 19 | 20 | /// A transformer that converts an arbitrarily-chunked byte stream where each 21 | /// packet is prefixed with a 32-bit little-endian number indicating its length 22 | /// into a stream of packet contents. 23 | final lengthDelimitedDecoder = 24 | StreamTransformer, Uint8List>.fromBind((stream) { 25 | // The number of bits we've consumed so far to fill out [nextMessageLength]. 26 | var nextMessageLengthBits = 0; 27 | 28 | // The length of the next message, in bytes. 29 | // 30 | // This is built up from a [varint]. Once it's fully consumed, [buffer] is 31 | // initialized. 32 | // 33 | // [varint]: https://developers.google.com/protocol-buffers/docs/encoding#varints 34 | var nextMessageLength = 0; 35 | 36 | // The buffer into which the packet data itself is written. Initialized once 37 | // [nextMessageLength] is known. 38 | Uint8List? buffer; 39 | 40 | // The index of the next byte to write to [buffer]. Once this is equal to 41 | // [buffer.length] (or equivalently [nextMessageLength]), the full packet is 42 | // available. 43 | var bufferIndex = 0; 44 | 45 | // It seems a little silly to use a nested [StreamTransformer] here, but we 46 | // need the outer one to establish a closure context so we can share state 47 | // across different input chunks, and the inner one takes care of all the 48 | // boilerplate of creating a new stream based on [stream]. 49 | return stream 50 | .transform(StreamTransformer.fromHandlers(handleData: (chunk, sink) { 51 | // The index of the next byte to read from [chunk]. We have to track this 52 | // because the chunk may contain the length *and* the message, or even 53 | // multiple messages. 54 | var i = 0; 55 | 56 | while (i < chunk.length) { 57 | var buffer_ = buffer; // dart-lang/language#1536 58 | 59 | // We can be in one of two states here: 60 | // 61 | // * [buffer] is `null`, in which case we're adding data to 62 | // [nextMessageLength] until we reach a byte with its most significant 63 | // bit set to 0. 64 | // 65 | // * [buffer] is not `null`, in which case we're waiting for [buffer] to 66 | // have [nextMessageLength] bytes in it before we send it to 67 | // [queue.local.sink] and start waiting for the next message. 68 | if (buffer_ == null) { 69 | var byte = chunk[i]; 70 | 71 | // Varints encode data in the 7 lower bits of each byte, which we access 72 | // by masking with 0x7f = 0b01111111. 73 | nextMessageLength += (byte & 0x7f) << nextMessageLengthBits; 74 | nextMessageLengthBits += 7; 75 | i++; 76 | 77 | // If the byte is higher than 0x7f = 0b01111111, that means its high bit 78 | // is set which and so there are more bytes to consume before we know 79 | // the full message length. 80 | if (byte > 0x7f) continue; 81 | 82 | // Otherwise, [nextMessageLength] is now finalized and we can allocate 83 | // the data buffer. 84 | buffer_ = buffer = Uint8List(nextMessageLength); 85 | bufferIndex = 0; 86 | } 87 | 88 | // Copy as many bytes as we can from [chunk] to [buffer], making sure not 89 | // to try to copy more than the buffer can hold (if the chunk has another 90 | // message after the current one) or more than the chunk has available (if 91 | // the current message is split across multiple chunks). 92 | var bytesToWrite = 93 | math.min(buffer_.length - bufferIndex, chunk.length - i); 94 | buffer_.setRange(bufferIndex, bufferIndex + bytesToWrite, chunk, i); 95 | i += bytesToWrite; 96 | bufferIndex += bytesToWrite; 97 | if (bufferIndex < nextMessageLength) return; 98 | 99 | // Once we've filled the buffer, emit it and reset our state. 100 | sink.add(buffer_); 101 | nextMessageLength = 0; 102 | nextMessageLengthBits = 0; 103 | buffer = null; 104 | } 105 | })); 106 | }); 107 | 108 | /// A transformer that adds 32-bit little-endian numbers indicating the length 109 | /// of each packet, so that they can safely be sent over a medium that doesn't 110 | /// preserve packet boundaries. 111 | final lengthDelimitedEncoder = 112 | StreamTransformer>.fromHandlers( 113 | handleData: (message, sink) { 114 | var length = message.length; 115 | if (length == 0) { 116 | sink.add([0]); 117 | return; 118 | } 119 | 120 | // Write the length in varint format, 7 bits at a time from least to most 121 | // significant. 122 | var lengthBuffer = Uint8Buffer(); 123 | while (length > 0) { 124 | // The highest-order bit indicates whether more bytes are necessary to fully 125 | // express the number. The lower 7 bits indicate the number's value. 126 | lengthBuffer.add((length > 0x7f ? 0x80 : 0) | (length & 0x7f)); 127 | length >>= 7; 128 | } 129 | 130 | sink.add(Uint8List.view(lengthBuffer.buffer, 0, lengthBuffer.length)); 131 | sink.add(message); 132 | }); 133 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_api/sass_api.dart' as sass; 6 | import 'package:source_span/source_span.dart'; 7 | import 'package:term_glyph/term_glyph.dart' as term_glyph; 8 | 9 | import 'embedded_sass.pb.dart' as proto; 10 | import 'embedded_sass.pb.dart' hide SourceSpan; 11 | 12 | /// The special ID that indicates an error that's not associated with a specific 13 | /// inbound request ID. 14 | const errorId = 0xffffffff; 15 | 16 | /// Returns a [ProtocolError] indicating that a mandatory field with the given 17 | /// [fieldName] was missing. 18 | ProtocolError mandatoryError(String fieldName) => 19 | paramsError("Missing mandatory field $fieldName"); 20 | 21 | /// Returns a [ProtocolError] indicating that the parameters for an inbound 22 | /// message were invalid. 23 | ProtocolError paramsError(String message) => ProtocolError() 24 | // Set the ID to [errorId] by default. This will be overwritten by the 25 | // dispatcher if a request ID is available. 26 | ..id = errorId 27 | ..type = ProtocolErrorType.PARAMS 28 | ..message = message; 29 | 30 | /// Converts a Dart source span to a protocol buffer source span. 31 | proto.SourceSpan protofySpan(SourceSpan span) { 32 | var protoSpan = proto.SourceSpan() 33 | ..text = span.text 34 | ..start = _protofyLocation(span.start) 35 | ..end = _protofyLocation(span.end) 36 | ..url = span.sourceUrl?.toString() ?? ""; 37 | if (span is SourceSpanWithContext) protoSpan.context = span.context; 38 | return protoSpan; 39 | } 40 | 41 | /// Converts a Dart source location to a protocol buffer source location. 42 | SourceSpan_SourceLocation _protofyLocation(SourceLocation location) => 43 | SourceSpan_SourceLocation() 44 | ..offset = location.offset 45 | ..line = location.line 46 | ..column = location.column; 47 | 48 | /// Converts a protocol buffer syntax enum into a Sass API syntax enum. 49 | sass.Syntax syntaxToSyntax(Syntax syntax) { 50 | switch (syntax) { 51 | case Syntax.SCSS: 52 | return sass.Syntax.scss; 53 | case Syntax.INDENTED: 54 | return sass.Syntax.sass; 55 | case Syntax.CSS: 56 | return sass.Syntax.css; 57 | default: 58 | throw "Unknown syntax $syntax."; 59 | } 60 | } 61 | 62 | /// Returns [string] with every line indented [indentation] spaces. 63 | String indent(String string, int indentation) => 64 | string.split("\n").map((line) => (" " * indentation) + line).join("\n"); 65 | 66 | /// Returns the result of running [callback] with the global ASCII config set 67 | /// to [ascii]. 68 | T withGlyphs(T callback(), {required bool ascii}) { 69 | var currentConfig = term_glyph.ascii; 70 | term_glyph.ascii = ascii; 71 | var result = callback(); 72 | term_glyph.ascii = currentConfig; 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/value.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:sass_api/sass_api.dart' as sass; 6 | 7 | import 'dispatcher.dart'; 8 | import 'embedded_sass.pb.dart'; 9 | import 'function_registry.dart'; 10 | import 'host_callable.dart'; 11 | import 'utils.dart'; 12 | 13 | /// Converts [value] to its protocol buffer representation. 14 | /// 15 | /// The [functions] tracks the IDs of first-class functions so that the host can 16 | /// pass them back to the compiler. 17 | Value protofyValue(FunctionRegistry functions, sass.Value value) { 18 | var result = Value(); 19 | if (value is sass.SassString) { 20 | result.string = Value_String() 21 | ..text = value.text 22 | ..quoted = value.hasQuotes; 23 | } else if (value is sass.SassNumber) { 24 | var number = Value_Number()..value = value.value * 1.0; 25 | number.numerators.addAll(value.numeratorUnits); 26 | number.denominators.addAll(value.denominatorUnits); 27 | result.number = number; 28 | } else if (value is sass.SassColor) { 29 | // TODO(nweiz): If the color is represented as HSL internally, this coerces 30 | // it to RGB. Is it worth providing some visibility into its internal 31 | // representation so we can serialize without converting? 32 | result.rgbColor = Value_RgbColor() 33 | ..red = value.red 34 | ..green = value.green 35 | ..blue = value.blue 36 | ..alpha = value.alpha * 1.0; 37 | } else if (value is sass.SassList) { 38 | var list = Value_List() 39 | ..separator = _protofySeparator(value.separator) 40 | ..hasBrackets = value.hasBrackets 41 | ..contents.addAll( 42 | [for (var element in value.asList) protofyValue(functions, element)]); 43 | result.list = list; 44 | } else if (value is sass.SassMap) { 45 | var map = Value_Map(); 46 | value.contents.forEach((key, value) { 47 | map.entries.add(Value_Map_Entry() 48 | ..key = protofyValue(functions, key) 49 | ..value = protofyValue(functions, value)); 50 | }); 51 | result.map = map; 52 | } else if (value is sass.SassFunction) { 53 | result.compilerFunction = functions.protofy(value); 54 | } else if (value == sass.sassTrue) { 55 | result.singleton = SingletonValue.TRUE; 56 | } else if (value == sass.sassFalse) { 57 | result.singleton = SingletonValue.FALSE; 58 | } else if (value == sass.sassNull) { 59 | result.singleton = SingletonValue.NULL; 60 | } else { 61 | throw "Unknown Value $value"; 62 | } 63 | return result; 64 | } 65 | 66 | /// Converts [separator] to its protocol buffer representation. 67 | ListSeparator _protofySeparator(sass.ListSeparator separator) { 68 | switch (separator) { 69 | case sass.ListSeparator.comma: 70 | return ListSeparator.COMMA; 71 | case sass.ListSeparator.space: 72 | return ListSeparator.SPACE; 73 | case sass.ListSeparator.slash: 74 | return ListSeparator.SLASH; 75 | case sass.ListSeparator.undecided: 76 | return ListSeparator.UNDECIDED; 77 | default: 78 | throw "Unknown ListSeparator $separator"; 79 | } 80 | } 81 | 82 | /// Converts [value] to its Sass representation. 83 | /// 84 | /// The [functions] tracks the IDs of first-class functions so that they can be 85 | /// deserialized to their original references. 86 | sass.Value deprotofyValue(Dispatcher dispatcher, FunctionRegistry functions, 87 | int compilationId, Value value) { 88 | // Curry recursive calls to this function so we don't have to keep repeating 89 | // ourselves. 90 | deprotofy(Value value) => 91 | deprotofyValue(dispatcher, functions, compilationId, value); 92 | 93 | try { 94 | switch (value.whichValue()) { 95 | case Value_Value.string: 96 | return value.string.text.isEmpty 97 | ? sass.SassString.empty(quotes: value.string.quoted) 98 | : sass.SassString(value.string.text, quotes: value.string.quoted); 99 | 100 | case Value_Value.number: 101 | return sass.SassNumber.withUnits(value.number.value, 102 | numeratorUnits: value.number.numerators, 103 | denominatorUnits: value.number.denominators); 104 | 105 | case Value_Value.rgbColor: 106 | return sass.SassColor.rgb(value.rgbColor.red, value.rgbColor.green, 107 | value.rgbColor.blue, value.rgbColor.alpha); 108 | 109 | case Value_Value.hslColor: 110 | return sass.SassColor.hsl(value.hslColor.hue, value.hslColor.saturation, 111 | value.hslColor.lightness, value.hslColor.alpha); 112 | 113 | case Value_Value.list: 114 | var separator = _deprotofySeparator(value.list.separator); 115 | if (value.list.contents.isEmpty) { 116 | return sass.SassList.empty( 117 | separator: separator, brackets: value.list.hasBrackets); 118 | } 119 | 120 | var length = value.list.contents.length; 121 | if (separator == sass.ListSeparator.undecided && length > 1) { 122 | throw paramsError( 123 | "List $value can't have an undecided separator because it has " 124 | "$length elements"); 125 | } 126 | 127 | return sass.SassList([ 128 | for (var element in value.list.contents) deprotofy(element) 129 | ], separator, brackets: value.list.hasBrackets); 130 | 131 | case Value_Value.map: 132 | return value.map.entries.isEmpty 133 | ? const sass.SassMap.empty() 134 | : sass.SassMap({ 135 | for (var entry in value.map.entries) 136 | deprotofy(entry.key): deprotofy(entry.value) 137 | }); 138 | 139 | case Value_Value.compilerFunction: 140 | var id = value.compilerFunction.id; 141 | var function = functions[id]; 142 | if (function == null) { 143 | throw paramsError( 144 | "CompilerFunction.id $id doesn't match any known functions"); 145 | } 146 | 147 | return function; 148 | 149 | case Value_Value.hostFunction: 150 | return sass.SassFunction(hostCallable( 151 | dispatcher, functions, compilationId, value.hostFunction.signature, 152 | id: value.hostFunction.id)); 153 | 154 | case Value_Value.singleton: 155 | switch (value.singleton) { 156 | case SingletonValue.TRUE: 157 | return sass.sassTrue; 158 | case SingletonValue.FALSE: 159 | return sass.sassFalse; 160 | case SingletonValue.NULL: 161 | return sass.sassNull; 162 | default: 163 | throw "Unknown Value.singleton ${value.singleton}"; 164 | } 165 | 166 | case Value_Value.notSet: 167 | default: 168 | throw mandatoryError("Value.value"); 169 | } 170 | } on RangeError catch (error) { 171 | var name = error.name; 172 | if (name == null || error.start == null || error.end == null) { 173 | throw paramsError(error.toString()); 174 | } 175 | 176 | if (value.whichValue() == Value_Value.rgbColor) { 177 | name = 'RgbColor.$name'; 178 | } else if (value.whichValue() == Value_Value.hslColor) { 179 | name = 'HslColor.$name'; 180 | } 181 | 182 | throw paramsError( 183 | '$name must be between ${error.start} and ${error.end}, was ' 184 | '${error.invalidValue}'); 185 | } 186 | } 187 | 188 | /// Converts [separator] to its Sass representation. 189 | sass.ListSeparator _deprotofySeparator(ListSeparator separator) { 190 | switch (separator) { 191 | case ListSeparator.COMMA: 192 | return sass.ListSeparator.comma; 193 | case ListSeparator.SPACE: 194 | return sass.ListSeparator.space; 195 | case ListSeparator.SLASH: 196 | return sass.ListSeparator.slash; 197 | case ListSeparator.UNDECIDED: 198 | return sass.ListSeparator.undecided; 199 | default: 200 | throw "Unknown separator $separator"; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "59.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "5.11.1" 20 | archive: 21 | dependency: transitive 22 | description: 23 | name: archive 24 | sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "3.3.7" 28 | args: 29 | dependency: transitive 30 | description: 31 | name: args 32 | sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.4.0" 36 | async: 37 | dependency: "direct main" 38 | description: 39 | name: async 40 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.11.0" 44 | boolean_selector: 45 | dependency: transitive 46 | description: 47 | name: boolean_selector 48 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.1.1" 52 | charcode: 53 | dependency: transitive 54 | description: 55 | name: charcode 56 | sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | checked_yaml: 61 | dependency: transitive 62 | description: 63 | name: checked_yaml 64 | sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.0.2" 68 | cli_pkg: 69 | dependency: "direct dev" 70 | description: 71 | name: cli_pkg 72 | sha256: "0f76b0ea3f158e9c68e3ae132e90435cfd094c507ae6aaeccb05bbc2ef758517" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "2.4.4" 76 | cli_repl: 77 | dependency: transitive 78 | description: 79 | name: cli_repl 80 | sha256: a2ee06d98f211cb960c777519cb3d14e882acd90fe5e078668e3ab4baab0ddd4 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "0.2.3" 84 | cli_util: 85 | dependency: transitive 86 | description: 87 | name: cli_util 88 | sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "0.3.5" 92 | collection: 93 | dependency: transitive 94 | description: 95 | name: collection 96 | sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "1.17.1" 100 | convert: 101 | dependency: transitive 102 | description: 103 | name: convert 104 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "3.1.1" 108 | coverage: 109 | dependency: transitive 110 | description: 111 | name: coverage 112 | sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.6.3" 116 | crypto: 117 | dependency: transitive 118 | description: 119 | name: crypto 120 | sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "3.0.2" 124 | dart_style: 125 | dependency: transitive 126 | description: 127 | name: dart_style 128 | sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "2.3.0" 132 | file: 133 | dependency: transitive 134 | description: 135 | name: file 136 | sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "6.1.4" 140 | fixnum: 141 | dependency: transitive 142 | description: 143 | name: fixnum 144 | sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.1.0" 148 | frontend_server_client: 149 | dependency: transitive 150 | description: 151 | name: frontend_server_client 152 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "3.2.0" 156 | glob: 157 | dependency: transitive 158 | description: 159 | name: glob 160 | sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "2.1.1" 164 | grinder: 165 | dependency: "direct dev" 166 | description: 167 | name: grinder 168 | sha256: "1dabcd70f0d3975f9143d0cebf48a09cf56d4f5e0922f1d1931781e1047c8d00" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "0.9.3" 172 | http: 173 | dependency: transitive 174 | description: 175 | name: http 176 | sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "0.13.5" 180 | http_multi_server: 181 | dependency: transitive 182 | description: 183 | name: http_multi_server 184 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "3.2.1" 188 | http_parser: 189 | dependency: transitive 190 | description: 191 | name: http_parser 192 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "4.0.2" 196 | io: 197 | dependency: transitive 198 | description: 199 | name: io 200 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "1.0.4" 204 | js: 205 | dependency: transitive 206 | description: 207 | name: js 208 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "0.6.7" 212 | json_annotation: 213 | dependency: transitive 214 | description: 215 | name: json_annotation 216 | sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "4.8.0" 220 | lints: 221 | dependency: transitive 222 | description: 223 | name: lints 224 | sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "2.0.1" 228 | logging: 229 | dependency: transitive 230 | description: 231 | name: logging 232 | sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "1.1.1" 236 | matcher: 237 | dependency: transitive 238 | description: 239 | name: matcher 240 | sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "0.12.15" 244 | meta: 245 | dependency: "direct main" 246 | description: 247 | name: meta 248 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "1.9.1" 252 | mime: 253 | dependency: transitive 254 | description: 255 | name: mime 256 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "1.0.4" 260 | node_interop: 261 | dependency: transitive 262 | description: 263 | name: node_interop 264 | sha256: "3af2420c728173806f4378cf89c53ba9f27f7f67792b898561bff9d390deb98e" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "2.1.0" 268 | node_preamble: 269 | dependency: transitive 270 | description: 271 | name: node_preamble 272 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "2.0.2" 276 | package_config: 277 | dependency: transitive 278 | description: 279 | name: package_config 280 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "2.1.0" 284 | path: 285 | dependency: "direct main" 286 | description: 287 | name: path 288 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "1.8.3" 292 | petitparser: 293 | dependency: transitive 294 | description: 295 | name: petitparser 296 | sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "5.4.0" 300 | pointycastle: 301 | dependency: transitive 302 | description: 303 | name: pointycastle 304 | sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "3.7.3" 308 | pool: 309 | dependency: transitive 310 | description: 311 | name: pool 312 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "1.5.1" 316 | protobuf: 317 | dependency: "direct main" 318 | description: 319 | name: protobuf 320 | sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "2.1.0" 324 | protoc_plugin: 325 | dependency: "direct dev" 326 | description: 327 | name: protoc_plugin 328 | sha256: e2be5014ba145dc0f8de20ac425afa2a513aff64fe350d338e481d40de0573df 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "20.0.1" 332 | pub_semver: 333 | dependency: "direct dev" 334 | description: 335 | name: pub_semver 336 | sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "2.1.3" 340 | pubspec_parse: 341 | dependency: "direct dev" 342 | description: 343 | name: pubspec_parse 344 | sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.2.2" 348 | retry: 349 | dependency: transitive 350 | description: 351 | name: retry 352 | sha256: a8a1e475a100a0bdc73f529ca8ea1e9c9c76bec8ad86a1f47780139a34ce7aea 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "3.1.1" 356 | sass: 357 | dependency: "direct main" 358 | description: 359 | name: sass 360 | sha256: cb18c7093e4e6ebabba6efbcbb9926238ca9932e3bf6c553dc811a89e1524b28 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "1.62.1" 364 | sass_analysis: 365 | dependency: "direct dev" 366 | description: 367 | path: analysis 368 | ref: HEAD 369 | resolved-ref: "8dddcb7b7db13984fea69fa85438acf30b56b4bb" 370 | url: "https://github.com/sass/dart-sass.git" 371 | source: git 372 | version: "0.0.0" 373 | sass_api: 374 | dependency: "direct main" 375 | description: 376 | name: sass_api 377 | sha256: "6901d0dd784a981946f7eb2cfc6cf2c3e24662230048375d28f4859797907052" 378 | url: "https://pub.dev" 379 | source: hosted 380 | version: "7.0.0" 381 | shelf: 382 | dependency: transitive 383 | description: 384 | name: shelf 385 | sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c 386 | url: "https://pub.dev" 387 | source: hosted 388 | version: "1.4.0" 389 | shelf_packages_handler: 390 | dependency: transitive 391 | description: 392 | name: shelf_packages_handler 393 | sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 394 | url: "https://pub.dev" 395 | source: hosted 396 | version: "3.0.1" 397 | shelf_static: 398 | dependency: transitive 399 | description: 400 | name: shelf_static 401 | sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c 402 | url: "https://pub.dev" 403 | source: hosted 404 | version: "1.1.1" 405 | shelf_web_socket: 406 | dependency: transitive 407 | description: 408 | name: shelf_web_socket 409 | sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 410 | url: "https://pub.dev" 411 | source: hosted 412 | version: "1.0.3" 413 | source_map_stack_trace: 414 | dependency: transitive 415 | description: 416 | name: source_map_stack_trace 417 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 418 | url: "https://pub.dev" 419 | source: hosted 420 | version: "2.1.1" 421 | source_maps: 422 | dependency: "direct dev" 423 | description: 424 | name: source_maps 425 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 426 | url: "https://pub.dev" 427 | source: hosted 428 | version: "0.10.12" 429 | source_span: 430 | dependency: "direct main" 431 | description: 432 | name: source_span 433 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 434 | url: "https://pub.dev" 435 | source: hosted 436 | version: "1.10.0" 437 | stack_trace: 438 | dependency: "direct main" 439 | description: 440 | name: stack_trace 441 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 442 | url: "https://pub.dev" 443 | source: hosted 444 | version: "1.11.0" 445 | stream_channel: 446 | dependency: "direct main" 447 | description: 448 | name: stream_channel 449 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 450 | url: "https://pub.dev" 451 | source: hosted 452 | version: "2.1.1" 453 | stream_transform: 454 | dependency: transitive 455 | description: 456 | name: stream_transform 457 | sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" 458 | url: "https://pub.dev" 459 | source: hosted 460 | version: "2.1.0" 461 | string_scanner: 462 | dependency: transitive 463 | description: 464 | name: string_scanner 465 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 466 | url: "https://pub.dev" 467 | source: hosted 468 | version: "1.2.0" 469 | term_glyph: 470 | dependency: "direct main" 471 | description: 472 | name: term_glyph 473 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 474 | url: "https://pub.dev" 475 | source: hosted 476 | version: "1.2.1" 477 | test: 478 | dependency: "direct dev" 479 | description: 480 | name: test 481 | sha256: "4f92f103ef63b1bbac6f4bd1930624fca81b2574464482512c4f0896319be575" 482 | url: "https://pub.dev" 483 | source: hosted 484 | version: "1.24.2" 485 | test_api: 486 | dependency: transitive 487 | description: 488 | name: test_api 489 | sha256: daadc9baabec998b062c9091525aa95786508b1c48e9c30f1f891b8bf6ff2e64 490 | url: "https://pub.dev" 491 | source: hosted 492 | version: "0.5.2" 493 | test_core: 494 | dependency: transitive 495 | description: 496 | name: test_core 497 | sha256: "3642b184882f79e76ca57a9230fb971e494c3c1fd09c21ae3083ce891bcc0aa1" 498 | url: "https://pub.dev" 499 | source: hosted 500 | version: "0.5.2" 501 | test_descriptor: 502 | dependency: "direct dev" 503 | description: 504 | name: test_descriptor 505 | sha256: abe245e8b0d61245684127fe32343542c25dc2a1ce8f405531637241d98d07e4 506 | url: "https://pub.dev" 507 | source: hosted 508 | version: "2.0.1" 509 | test_process: 510 | dependency: transitive 511 | description: 512 | name: test_process 513 | sha256: b0e6702f58272d459d5b80b88696483f86eac230dab05ecf73d0662e305d005e 514 | url: "https://pub.dev" 515 | source: hosted 516 | version: "2.0.3" 517 | tuple: 518 | dependency: transitive 519 | description: 520 | name: tuple 521 | sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" 522 | url: "https://pub.dev" 523 | source: hosted 524 | version: "2.0.1" 525 | typed_data: 526 | dependency: "direct main" 527 | description: 528 | name: typed_data 529 | sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" 530 | url: "https://pub.dev" 531 | source: hosted 532 | version: "1.3.1" 533 | vm_service: 534 | dependency: transitive 535 | description: 536 | name: vm_service 537 | sha256: "518254c0d3ee20667a1feef39eefe037df87439851e4b3cb277e5b3f37afa2f0" 538 | url: "https://pub.dev" 539 | source: hosted 540 | version: "11.4.0" 541 | watcher: 542 | dependency: transitive 543 | description: 544 | name: watcher 545 | sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" 546 | url: "https://pub.dev" 547 | source: hosted 548 | version: "1.0.2" 549 | web_socket_channel: 550 | dependency: transitive 551 | description: 552 | name: web_socket_channel 553 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 554 | url: "https://pub.dev" 555 | source: hosted 556 | version: "2.4.0" 557 | webkit_inspection_protocol: 558 | dependency: transitive 559 | description: 560 | name: webkit_inspection_protocol 561 | sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" 562 | url: "https://pub.dev" 563 | source: hosted 564 | version: "1.2.0" 565 | xml: 566 | dependency: transitive 567 | description: 568 | name: xml 569 | sha256: "80d494c09849dc3f899d227a78c30c5b949b985ededf884cb3f3bcd39f4b447a" 570 | url: "https://pub.dev" 571 | source: hosted 572 | version: "5.4.1" 573 | yaml: 574 | dependency: "direct dev" 575 | description: 576 | name: yaml 577 | sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" 578 | url: "https://pub.dev" 579 | source: hosted 580 | version: "3.1.1" 581 | sdks: 582 | dart: ">=2.19.0 <3.0.0" 583 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sass_embedded 2 | version: 1.62.1 3 | description: An implementation of the Sass embedded protocol using Dart Sass. 4 | homepage: https://github.com/sass/dart-sass-embedded 5 | environment: 6 | sdk: '>=2.12.0 <3.0.0' 7 | executables: 8 | dart-sass-embedded: dart_sass_embedded 9 | dependencies: 10 | async: ">=1.13.0 <3.0.0" 11 | meta: ^1.1.0 12 | path: ^1.6.0 13 | protobuf: ^2.0.0 14 | sass: 1.62.1 15 | sass_api: ^7.0.0 16 | source_span: ^1.1.0 17 | stack_trace: ^1.6.0 18 | stream_channel: ">=1.6.0 <3.0.0" 19 | term_glyph: ^1.0.0 20 | typed_data: ^1.1.0 21 | dev_dependencies: 22 | cli_pkg: ^2.1.0 23 | grinder: ^0.9.0 24 | protoc_plugin: ^20.0.0 25 | test: ^1.0.0 26 | test_descriptor: ^2.0.0 27 | yaml: ^3.1.0 28 | pubspec_parse: ^1.0.0 29 | pub_semver: ^2.0.0 30 | sass_analysis: 31 | git: 32 | url: https://github.com/sass/dart-sass.git 33 | path: analysis 34 | source_maps: ^0.10.10 35 | -------------------------------------------------------------------------------- /test/dependencies_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:pubspec_parse/pubspec_parse.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | /// This package's pubspec. 11 | var _pubspec = Pubspec.parse(File('pubspec.yaml').readAsStringSync(), 12 | sourceUrl: Uri.parse('pubspec.yaml')); 13 | 14 | void main() { 15 | // Assert that our declared dependency on Dart Sass is either a Git dependency 16 | // or the same version as the version we're testing against. 17 | 18 | test('depends on a compatible version of Dart Sass', () { 19 | var sassDependency = _pubspec.dependencies['sass']; 20 | if (sassDependency is GitDependency) return; 21 | 22 | var actualVersion = 23 | (Process.runSync('dart', ['run', 'sass', '--version']).stdout as String) 24 | .trim(); 25 | expect(sassDependency, isA()); 26 | expect(actualVersion, 27 | equals((sassDependency as HostedDependency).version.toString())); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/embedded_process.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:io'; 8 | 9 | import 'package:async/async.dart'; 10 | import 'package:cli_pkg/testing.dart' as pkg; 11 | import 'package:test/test.dart'; 12 | 13 | import 'package:sass_embedded/src/embedded_sass.pb.dart'; 14 | import 'package:sass_embedded/src/util/length_delimited_transformer.dart'; 15 | 16 | /// A wrapper for [Process] that provides a convenient API for testing the 17 | /// embedded Sass process. 18 | /// 19 | /// If the test fails, this will automatically print out any stderr and protocol 20 | /// buffers from the process to aid debugging. 21 | /// 22 | /// This API is based on the `test_process` package. 23 | class EmbeddedProcess { 24 | /// The underlying process. 25 | final Process _process; 26 | 27 | /// A [StreamQueue] that emits each outbound protocol buffer from the process. 28 | StreamQueue get outbound => _outbound; 29 | late StreamQueue _outbound; 30 | 31 | /// A [StreamQueue] that emits each line of stderr from the process. 32 | StreamQueue get stderr => _stderr; 33 | late StreamQueue _stderr; 34 | 35 | /// A splitter that can emit new copies of [outbound]. 36 | final StreamSplitter _outboundSplitter; 37 | 38 | /// A splitter that can emit new copies of [stderr]. 39 | final StreamSplitter _stderrSplitter; 40 | 41 | /// A sink into which inbound messages can be passed to the process. 42 | final Sink inbound; 43 | 44 | /// The raw standard input byte sink. 45 | IOSink get stdin => _process.stdin; 46 | 47 | /// A log that includes lines from [stderr] and human-friendly serializations 48 | /// of protocol buffers from [outbound] 49 | final _log = []; 50 | 51 | /// Whether [_log] has been passed to [printOnFailure] yet. 52 | var _loggedOutput = false; 53 | 54 | /// Returns a [Future] which completes to the exit code of the process, once 55 | /// it completes. 56 | Future get exitCode => _process.exitCode; 57 | 58 | /// The process ID of the process. 59 | int get pid => _process.pid; 60 | 61 | /// Completes to [_process]'s exit code if it's exited, otherwise completes to 62 | /// `null` immediately. 63 | Future get _exitCodeOrNull async { 64 | var exitCode = 65 | await this.exitCode.timeout(Duration.zero, onTimeout: () => -1); 66 | return exitCode == -1 ? null : exitCode; 67 | } 68 | 69 | /// Starts a process. 70 | /// 71 | /// [executable], [workingDirectory], [environment], 72 | /// [includeParentEnvironment], and [runInShell] have the same meaning as for 73 | /// [Process.start]. 74 | /// 75 | /// If [forwardOutput] is `true`, the process's [outbound] messages and 76 | /// [stderr] will be printed to the console as they appear. This is only 77 | /// intended to be set temporarily to help when debugging test failures. 78 | static Future start( 79 | {String? workingDirectory, 80 | Map? environment, 81 | bool includeParentEnvironment = true, 82 | bool runInShell = false, 83 | bool forwardOutput = false}) async { 84 | var process = await Process.start( 85 | pkg.executableRunner("dart-sass-embedded"), 86 | pkg.executableArgs("dart-sass-embedded"), 87 | workingDirectory: workingDirectory, 88 | environment: environment, 89 | includeParentEnvironment: includeParentEnvironment, 90 | runInShell: runInShell); 91 | 92 | return EmbeddedProcess._(process, forwardOutput: forwardOutput); 93 | } 94 | 95 | /// Creates a [EmbeddedProcess] for [process]. 96 | /// 97 | /// The [forwardOutput] argument is the same as that to [start]. 98 | EmbeddedProcess._(Process process, {bool forwardOutput = false}) 99 | : _process = process, 100 | _outboundSplitter = StreamSplitter(process.stdout 101 | .transform(lengthDelimitedDecoder) 102 | .map((message) => OutboundMessage.fromBuffer(message))), 103 | _stderrSplitter = StreamSplitter(process.stderr 104 | .transform(utf8.decoder) 105 | .transform(const LineSplitter())), 106 | inbound = StreamSinkTransformer>.fromHandlers( 107 | handleData: (message, sink) => 108 | sink.add(message.writeToBuffer())).bind( 109 | StreamSinkTransformer.fromStreamTransformer(lengthDelimitedEncoder) 110 | .bind(process.stdin)) { 111 | addTearDown(_tearDown); 112 | expect(_process.exitCode.then((_) => _logOutput()), completes, 113 | reason: "Process `dart_sass_embedded` never exited."); 114 | 115 | _outbound = StreamQueue(_outboundSplitter.split()); 116 | _stderr = StreamQueue(_stderrSplitter.split()); 117 | 118 | _outboundSplitter.split().listen((message) { 119 | for (var line in message.toDebugString().split("\n")) { 120 | if (forwardOutput) print(line); 121 | _log.add(" $line"); 122 | } 123 | }); 124 | 125 | _stderrSplitter.split().listen((line) { 126 | if (forwardOutput) print(line); 127 | _log.add("[e] $line"); 128 | }); 129 | } 130 | 131 | /// A callback that's run when the test completes. 132 | Future _tearDown() async { 133 | // If the process is already dead, do nothing. 134 | if (await _exitCodeOrNull != null) return; 135 | 136 | _process.kill(ProcessSignal.sigkill); 137 | 138 | // Log output now rather than waiting for the exitCode callback so that 139 | // it's visible even if we time out waiting for the process to die. 140 | await _logOutput(); 141 | } 142 | 143 | /// Formats the contents of [_log] and passes them to [printOnFailure]. 144 | Future _logOutput() async { 145 | if (_loggedOutput) return; 146 | _loggedOutput = true; 147 | 148 | var exitCodeOrNull = await _exitCodeOrNull; 149 | 150 | // Wait a timer tick to ensure that all available lines have been flushed to 151 | // [_log]. 152 | await Future.delayed(Duration.zero); 153 | 154 | var buffer = StringBuffer(); 155 | buffer.write("Process `dart_sass_embedded` "); 156 | if (exitCodeOrNull == null) { 157 | buffer.write("was killed with SIGKILL in a tear-down."); 158 | } else { 159 | buffer.write("exited with exitCode $exitCodeOrNull."); 160 | } 161 | buffer.writeln(" Output:"); 162 | buffer.writeln(_log.join("\n")); 163 | 164 | printOnFailure(buffer.toString()); 165 | } 166 | 167 | /// Kills the process (with SIGKILL on POSIX operating systems), and returns a 168 | /// future that completes once it's dead. 169 | /// 170 | /// If this is called after the process is already dead, it does nothing. 171 | Future kill() async { 172 | _process.kill(ProcessSignal.sigkill); 173 | await exitCode; 174 | } 175 | 176 | /// Waits for the process to exit, and verifies that the exit code matches 177 | /// [expectedExitCode] (if given). 178 | /// 179 | /// If this is called after the process is already dead, it verifies its 180 | /// existing exit code. 181 | Future shouldExit([int? expectedExitCode]) async { 182 | var exitCode = await this.exitCode; 183 | if (expectedExitCode == null) return; 184 | expect(exitCode, expectedExitCode, 185 | reason: "Process `dart_sass_embedded` had an unexpected exit code."); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /test/file_importer_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:path/path.dart' as p; 6 | import 'package:test/test.dart'; 7 | import 'package:test_descriptor/test_descriptor.dart' as d; 8 | 9 | import 'package:sass_embedded/src/embedded_sass.pb.dart'; 10 | import 'package:sass_embedded/src/utils.dart'; 11 | 12 | import 'embedded_process.dart'; 13 | import 'utils.dart'; 14 | 15 | void main() { 16 | late EmbeddedProcess process; 17 | setUp(() async { 18 | process = await EmbeddedProcess.start(); 19 | }); 20 | 21 | group("emits a protocol error", () { 22 | late OutboundMessage_FileImportRequest request; 23 | 24 | setUp(() async { 25 | process.inbound.add(compileString("@import 'other'", importers: [ 26 | InboundMessage_CompileRequest_Importer()..fileImporterId = 1 27 | ])); 28 | 29 | request = getFileImportRequest(await process.outbound.next); 30 | }); 31 | 32 | test("for a response without a corresponding request ID", () async { 33 | process.inbound.add(InboundMessage() 34 | ..fileImportResponse = 35 | (InboundMessage_FileImportResponse()..id = request.id + 1)); 36 | 37 | await expectParamsError( 38 | process, 39 | errorId, 40 | "Response ID ${request.id + 1} doesn't match any outstanding " 41 | "requests."); 42 | await process.kill(); 43 | }); 44 | 45 | test("for a response that doesn't match the request type", () async { 46 | process.inbound.add(InboundMessage() 47 | ..canonicalizeResponse = 48 | (InboundMessage_CanonicalizeResponse()..id = request.id)); 49 | 50 | await expectParamsError( 51 | process, 52 | errorId, 53 | "Request ID ${request.id} doesn't match response type " 54 | "InboundMessage_CanonicalizeResponse."); 55 | await process.kill(); 56 | }); 57 | }); 58 | 59 | group("emits a compile failure", () { 60 | late OutboundMessage_FileImportRequest request; 61 | 62 | setUp(() async { 63 | process.inbound.add(compileString("@import 'other'", importers: [ 64 | InboundMessage_CompileRequest_Importer()..fileImporterId = 1 65 | ])); 66 | 67 | request = getFileImportRequest(await process.outbound.next); 68 | }); 69 | 70 | group("for a FileImportResponse with a URL", () { 71 | test("that's empty", () async { 72 | process.inbound.add(InboundMessage() 73 | ..fileImportResponse = (InboundMessage_FileImportResponse() 74 | ..id = request.id 75 | ..fileUrl = "")); 76 | 77 | await _expectImportError( 78 | process, 'The file importer must return an absolute URL, was ""'); 79 | await process.kill(); 80 | }); 81 | 82 | test("that's relative", () async { 83 | process.inbound.add(InboundMessage() 84 | ..fileImportResponse = (InboundMessage_FileImportResponse() 85 | ..id = request.id 86 | ..fileUrl = "foo")); 87 | 88 | await _expectImportError(process, 89 | 'The file importer must return an absolute URL, was "foo"'); 90 | await process.kill(); 91 | }); 92 | 93 | test("that's not file:", () async { 94 | process.inbound.add(InboundMessage() 95 | ..fileImportResponse = (InboundMessage_FileImportResponse() 96 | ..id = request.id 97 | ..fileUrl = "other:foo")); 98 | 99 | await _expectImportError(process, 100 | 'The file importer must return a file: URL, was "other:foo"'); 101 | await process.kill(); 102 | }); 103 | }); 104 | }); 105 | 106 | group("includes in FileImportRequest", () { 107 | var compilationId = 1234; 108 | var importerId = 5679; 109 | late OutboundMessage_FileImportRequest request; 110 | setUp(() async { 111 | process.inbound.add( 112 | compileString("@import 'other'", id: compilationId, importers: [ 113 | InboundMessage_CompileRequest_Importer()..fileImporterId = importerId 114 | ])); 115 | request = getFileImportRequest(await process.outbound.next); 116 | }); 117 | 118 | test("the same compilationId as the compilation", () async { 119 | expect(request.compilationId, equals(compilationId)); 120 | await process.kill(); 121 | }); 122 | 123 | test("a known importerId", () async { 124 | expect(request.importerId, equals(importerId)); 125 | await process.kill(); 126 | }); 127 | 128 | test("the imported URL", () async { 129 | expect(request.url, equals("other")); 130 | await process.kill(); 131 | }); 132 | 133 | test("whether the import came from an @import", () async { 134 | expect(request.fromImport, isTrue); 135 | await process.kill(); 136 | }); 137 | }); 138 | 139 | test("errors cause compilation to fail", () async { 140 | process.inbound.add(compileString("@import 'other'", importers: [ 141 | InboundMessage_CompileRequest_Importer()..fileImporterId = 1 142 | ])); 143 | 144 | var request = getFileImportRequest(await process.outbound.next); 145 | process.inbound.add(InboundMessage() 146 | ..fileImportResponse = (InboundMessage_FileImportResponse() 147 | ..id = request.id 148 | ..error = "oh no")); 149 | 150 | var failure = getCompileFailure(await process.outbound.next); 151 | expect(failure.message, equals('oh no')); 152 | expect(failure.span.text, equals("'other'")); 153 | expect(failure.stackTrace, equals('- 1:9 root stylesheet\n')); 154 | await process.kill(); 155 | }); 156 | 157 | test("null results count as not found", () async { 158 | process.inbound.add(compileString("@import 'other'", importers: [ 159 | InboundMessage_CompileRequest_Importer()..fileImporterId = 1 160 | ])); 161 | 162 | var request = getFileImportRequest(await process.outbound.next); 163 | process.inbound.add(InboundMessage() 164 | ..fileImportResponse = 165 | (InboundMessage_FileImportResponse()..id = request.id)); 166 | 167 | var failure = getCompileFailure(await process.outbound.next); 168 | expect(failure.message, equals("Can't find stylesheet to import.")); 169 | expect(failure.span.text, equals("'other'")); 170 | await process.kill(); 171 | }); 172 | 173 | group("attempts importers in order", () { 174 | test("with multiple file importers", () async { 175 | process.inbound.add(compileString("@import 'other'", importers: [ 176 | for (var i = 0; i < 10; i++) 177 | InboundMessage_CompileRequest_Importer()..fileImporterId = i 178 | ])); 179 | 180 | for (var i = 0; i < 10; i++) { 181 | var request = getFileImportRequest(await process.outbound.next); 182 | expect(request.importerId, equals(i)); 183 | process.inbound.add(InboundMessage() 184 | ..fileImportResponse = 185 | (InboundMessage_FileImportResponse()..id = request.id)); 186 | } 187 | 188 | await process.kill(); 189 | }); 190 | 191 | test("with a mixture of file and normal importers", () async { 192 | process.inbound.add(compileString("@import 'other'", importers: [ 193 | for (var i = 0; i < 10; i++) 194 | if (i % 2 == 0) 195 | InboundMessage_CompileRequest_Importer()..fileImporterId = i 196 | else 197 | InboundMessage_CompileRequest_Importer()..importerId = i 198 | ])); 199 | 200 | for (var i = 0; i < 10; i++) { 201 | if (i % 2 == 0) { 202 | var request = getFileImportRequest(await process.outbound.next); 203 | expect(request.importerId, equals(i)); 204 | process.inbound.add(InboundMessage() 205 | ..fileImportResponse = 206 | (InboundMessage_FileImportResponse()..id = request.id)); 207 | } else { 208 | var request = getCanonicalizeRequest(await process.outbound.next); 209 | expect(request.importerId, equals(i)); 210 | process.inbound.add(InboundMessage() 211 | ..canonicalizeResponse = 212 | (InboundMessage_CanonicalizeResponse()..id = request.id)); 213 | } 214 | } 215 | 216 | await process.kill(); 217 | }); 218 | }); 219 | 220 | test("tries resolved URL as a relative path first", () async { 221 | await d.file("upstream.scss", "a {b: c}").create(); 222 | await d.file("midstream.scss", "@import 'upstream';").create(); 223 | 224 | process.inbound.add(compileString("@import 'midstream'", importers: [ 225 | for (var i = 0; i < 10; i++) 226 | InboundMessage_CompileRequest_Importer()..fileImporterId = i 227 | ])); 228 | 229 | for (var i = 0; i < 5; i++) { 230 | var request = getFileImportRequest(await process.outbound.next); 231 | expect(request.url, equals("midstream")); 232 | expect(request.importerId, equals(i)); 233 | process.inbound.add(InboundMessage() 234 | ..fileImportResponse = 235 | (InboundMessage_FileImportResponse()..id = request.id)); 236 | } 237 | 238 | var request = getFileImportRequest(await process.outbound.next); 239 | expect(request.importerId, equals(5)); 240 | process.inbound.add(InboundMessage() 241 | ..fileImportResponse = (InboundMessage_FileImportResponse() 242 | ..id = request.id 243 | ..fileUrl = p.toUri(d.path("midstream")).toString())); 244 | 245 | await expectLater(process.outbound, emits(isSuccess("a { b: c; }"))); 246 | await process.kill(); 247 | }); 248 | 249 | group("handles an importer for a string compile request", () { 250 | setUp(() async { 251 | await d.file("other.scss", "a {b: c}").create(); 252 | }); 253 | 254 | test("without a base URL", () async { 255 | process.inbound.add(compileString("@import 'other'", 256 | importer: InboundMessage_CompileRequest_Importer() 257 | ..fileImporterId = 1)); 258 | 259 | var request = getFileImportRequest(await process.outbound.next); 260 | expect(request.url, equals("other")); 261 | 262 | process.inbound.add(InboundMessage() 263 | ..fileImportResponse = (InboundMessage_FileImportResponse() 264 | ..id = request.id 265 | ..fileUrl = p.toUri(d.path("other")).toString())); 266 | 267 | await expectLater(process.outbound, emits(isSuccess("a { b: c; }"))); 268 | await process.kill(); 269 | }); 270 | 271 | test("with a base URL", () async { 272 | process.inbound.add(compileString("@import 'other'", 273 | url: p.toUri(d.path("input")).toString(), 274 | importer: InboundMessage_CompileRequest_Importer() 275 | ..fileImporterId = 1)); 276 | 277 | await expectLater(process.outbound, emits(isSuccess("a { b: c; }"))); 278 | await process.kill(); 279 | }); 280 | }); 281 | } 282 | 283 | /// Asserts that [process] emits a [CompileFailure] result with the given 284 | /// [message] on its protobuf stream and causes the compilation to fail. 285 | Future _expectImportError(EmbeddedProcess process, Object message) async { 286 | var failure = getCompileFailure(await process.outbound.next); 287 | expect(failure.message, equals(message)); 288 | expect(failure.span.text, equals("'other'")); 289 | } 290 | -------------------------------------------------------------------------------- /test/importer_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:source_maps/source_maps.dart' as source_maps; 6 | import 'package:test/test.dart'; 7 | import 'package:test_descriptor/test_descriptor.dart' as d; 8 | 9 | import 'package:sass_embedded/src/embedded_sass.pb.dart'; 10 | import 'package:sass_embedded/src/utils.dart'; 11 | 12 | import 'embedded_process.dart'; 13 | import 'utils.dart'; 14 | 15 | void main() { 16 | late EmbeddedProcess process; 17 | setUp(() async { 18 | process = await EmbeddedProcess.start(); 19 | }); 20 | 21 | group("emits a protocol error", () { 22 | test("for a response without a corresponding request ID", () async { 23 | process.inbound.add(compileString("@import 'other'", importers: [ 24 | InboundMessage_CompileRequest_Importer()..importerId = 1 25 | ])); 26 | 27 | var request = getCanonicalizeRequest(await process.outbound.next); 28 | process.inbound.add(InboundMessage() 29 | ..canonicalizeResponse = 30 | (InboundMessage_CanonicalizeResponse()..id = request.id + 1)); 31 | 32 | await expectParamsError( 33 | process, 34 | errorId, 35 | "Response ID ${request.id + 1} doesn't match any outstanding " 36 | "requests."); 37 | await process.kill(); 38 | }); 39 | 40 | test("for a response that doesn't match the request type", () async { 41 | process.inbound.add(compileString("@import 'other'", importers: [ 42 | InboundMessage_CompileRequest_Importer()..importerId = 1 43 | ])); 44 | 45 | var request = getCanonicalizeRequest(await process.outbound.next); 46 | process.inbound.add(InboundMessage() 47 | ..importResponse = (InboundMessage_ImportResponse()..id = request.id)); 48 | 49 | await expectParamsError( 50 | process, 51 | errorId, 52 | "Request ID ${request.id} doesn't match response type " 53 | "InboundMessage_ImportResponse."); 54 | await process.kill(); 55 | }); 56 | 57 | test("for an unset importer", () async { 58 | process.inbound.add(compileString("a {b: c}", 59 | importers: [InboundMessage_CompileRequest_Importer()])); 60 | await expectParamsError( 61 | process, 0, "Missing mandatory field Importer.importer"); 62 | await process.kill(); 63 | }); 64 | }); 65 | 66 | group("canonicalization", () { 67 | group("emits a compile failure", () { 68 | test("for a canonicalize response with an empty URL", () async { 69 | process.inbound.add(compileString("@import 'other'", importers: [ 70 | InboundMessage_CompileRequest_Importer()..importerId = 1 71 | ])); 72 | 73 | var request = getCanonicalizeRequest(await process.outbound.next); 74 | process.inbound.add(InboundMessage() 75 | ..canonicalizeResponse = (InboundMessage_CanonicalizeResponse() 76 | ..id = request.id 77 | ..url = "")); 78 | 79 | await _expectImportError( 80 | process, 'The importer must return an absolute URL, was ""'); 81 | await process.kill(); 82 | }); 83 | 84 | test("for a canonicalize response with a relative URL", () async { 85 | process.inbound.add(compileString("@import 'other'", importers: [ 86 | InboundMessage_CompileRequest_Importer()..importerId = 1 87 | ])); 88 | 89 | var request = getCanonicalizeRequest(await process.outbound.next); 90 | process.inbound.add(InboundMessage() 91 | ..canonicalizeResponse = (InboundMessage_CanonicalizeResponse() 92 | ..id = request.id 93 | ..url = "relative")); 94 | 95 | await _expectImportError(process, 96 | 'The importer must return an absolute URL, was "relative"'); 97 | await process.kill(); 98 | }); 99 | }); 100 | 101 | group("includes in CanonicalizeRequest", () { 102 | var compilationId = 1234; 103 | var importerId = 5679; 104 | late OutboundMessage_CanonicalizeRequest request; 105 | setUp(() async { 106 | process.inbound.add(compileString("@import 'other'", 107 | id: compilationId, 108 | importers: [ 109 | InboundMessage_CompileRequest_Importer()..importerId = importerId 110 | ])); 111 | request = getCanonicalizeRequest(await process.outbound.next); 112 | }); 113 | 114 | test("the same compilationId as the compilation", () async { 115 | expect(request.compilationId, equals(compilationId)); 116 | await process.kill(); 117 | }); 118 | 119 | test("a known importerId", () async { 120 | expect(request.importerId, equals(importerId)); 121 | await process.kill(); 122 | }); 123 | 124 | test("the imported URL", () async { 125 | expect(request.url, equals("other")); 126 | await process.kill(); 127 | }); 128 | }); 129 | 130 | test("errors cause compilation to fail", () async { 131 | process.inbound.add(compileString("@import 'other'", importers: [ 132 | InboundMessage_CompileRequest_Importer()..importerId = 1 133 | ])); 134 | 135 | var request = getCanonicalizeRequest(await process.outbound.next); 136 | process.inbound.add(InboundMessage() 137 | ..canonicalizeResponse = (InboundMessage_CanonicalizeResponse() 138 | ..id = request.id 139 | ..error = "oh no")); 140 | 141 | var failure = getCompileFailure(await process.outbound.next); 142 | expect(failure.message, equals('oh no')); 143 | expect(failure.span.text, equals("'other'")); 144 | expect(failure.stackTrace, equals('- 1:9 root stylesheet\n')); 145 | await process.kill(); 146 | }); 147 | 148 | test("null results count as not found", () async { 149 | process.inbound.add(compileString("@import 'other'", importers: [ 150 | InboundMessage_CompileRequest_Importer()..importerId = 1 151 | ])); 152 | 153 | var request = getCanonicalizeRequest(await process.outbound.next); 154 | process.inbound.add(InboundMessage() 155 | ..canonicalizeResponse = 156 | (InboundMessage_CanonicalizeResponse()..id = request.id)); 157 | 158 | var failure = getCompileFailure(await process.outbound.next); 159 | expect(failure.message, equals("Can't find stylesheet to import.")); 160 | expect(failure.span.text, equals("'other'")); 161 | await process.kill(); 162 | }); 163 | 164 | test("attempts importers in order", () async { 165 | process.inbound.add(compileString("@import 'other'", importers: [ 166 | for (var i = 0; i < 10; i++) 167 | InboundMessage_CompileRequest_Importer()..importerId = i 168 | ])); 169 | 170 | for (var i = 0; i < 10; i++) { 171 | var request = getCanonicalizeRequest(await process.outbound.next); 172 | expect(request.importerId, equals(i)); 173 | process.inbound.add(InboundMessage() 174 | ..canonicalizeResponse = 175 | (InboundMessage_CanonicalizeResponse()..id = request.id)); 176 | } 177 | 178 | await process.kill(); 179 | }); 180 | 181 | test("tries resolved URL using the original importer first", () async { 182 | process.inbound.add(compileString("@import 'midstream'", importers: [ 183 | for (var i = 0; i < 10; i++) 184 | InboundMessage_CompileRequest_Importer()..importerId = i 185 | ])); 186 | 187 | for (var i = 0; i < 5; i++) { 188 | var request = getCanonicalizeRequest(await process.outbound.next); 189 | expect(request.url, equals("midstream")); 190 | expect(request.importerId, equals(i)); 191 | process.inbound.add(InboundMessage() 192 | ..canonicalizeResponse = 193 | (InboundMessage_CanonicalizeResponse()..id = request.id)); 194 | } 195 | 196 | var canonicalize = getCanonicalizeRequest(await process.outbound.next); 197 | expect(canonicalize.importerId, equals(5)); 198 | process.inbound.add(InboundMessage() 199 | ..canonicalizeResponse = (InboundMessage_CanonicalizeResponse() 200 | ..id = canonicalize.id 201 | ..url = "custom:foo/bar")); 202 | 203 | var import = getImportRequest(await process.outbound.next); 204 | process.inbound.add(InboundMessage() 205 | ..importResponse = (InboundMessage_ImportResponse() 206 | ..id = import.id 207 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 208 | ..contents = "@import 'upstream'"))); 209 | 210 | canonicalize = getCanonicalizeRequest(await process.outbound.next); 211 | expect(canonicalize.importerId, equals(5)); 212 | expect(canonicalize.url, equals("custom:foo/upstream")); 213 | 214 | await process.kill(); 215 | }); 216 | }); 217 | 218 | group("importing", () { 219 | group("emits a compile failure", () { 220 | test("for an import result with a relative sourceMapUrl", () async { 221 | process.inbound.add(compileString("@import 'other'", importers: [ 222 | InboundMessage_CompileRequest_Importer()..importerId = 1 223 | ])); 224 | await _canonicalize(process); 225 | 226 | var import = getImportRequest(await process.outbound.next); 227 | process.inbound.add(InboundMessage() 228 | ..importResponse = (InboundMessage_ImportResponse() 229 | ..id = import.id 230 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 231 | ..sourceMapUrl = "relative"))); 232 | 233 | await _expectImportError(process, 234 | 'The importer must return an absolute URL, was "relative"'); 235 | await process.kill(); 236 | }); 237 | }); 238 | 239 | group("includes in ImportRequest", () { 240 | var compilationId = 1234; 241 | var importerId = 5678; 242 | late OutboundMessage_ImportRequest request; 243 | setUp(() async { 244 | process.inbound.add(compileString("@import 'other'", 245 | id: compilationId, 246 | importers: [ 247 | InboundMessage_CompileRequest_Importer()..importerId = importerId 248 | ])); 249 | 250 | var canonicalize = getCanonicalizeRequest(await process.outbound.next); 251 | process.inbound.add(InboundMessage() 252 | ..canonicalizeResponse = (InboundMessage_CanonicalizeResponse() 253 | ..id = canonicalize.id 254 | ..url = "custom:foo")); 255 | 256 | request = getImportRequest(await process.outbound.next); 257 | }); 258 | 259 | test("the same compilationId as the compilation", () async { 260 | expect(request.compilationId, equals(compilationId)); 261 | await process.kill(); 262 | }); 263 | 264 | test("a known importerId", () async { 265 | expect(request.importerId, equals(importerId)); 266 | await process.kill(); 267 | }); 268 | 269 | test("the canonical URL", () async { 270 | expect(request.url, equals("custom:foo")); 271 | await process.kill(); 272 | }); 273 | }); 274 | 275 | test("null results count as not found", () async { 276 | process.inbound.add(compileString("@import 'other'", importers: [ 277 | InboundMessage_CompileRequest_Importer()..importerId = 1 278 | ])); 279 | 280 | var canonicalizeRequest = 281 | getCanonicalizeRequest(await process.outbound.next); 282 | process.inbound.add(InboundMessage() 283 | ..canonicalizeResponse = (InboundMessage_CanonicalizeResponse() 284 | ..id = canonicalizeRequest.id 285 | ..url = "o:other")); 286 | 287 | var importRequest = getImportRequest(await process.outbound.next); 288 | process.inbound.add(InboundMessage() 289 | ..importResponse = 290 | (InboundMessage_ImportResponse()..id = importRequest.id)); 291 | 292 | var failure = getCompileFailure(await process.outbound.next); 293 | expect(failure.message, equals("Can't find stylesheet to import.")); 294 | expect(failure.span.text, equals("'other'")); 295 | await process.kill(); 296 | }); 297 | 298 | test("errors cause compilation to fail", () async { 299 | process.inbound.add(compileString("@import 'other'", importers: [ 300 | InboundMessage_CompileRequest_Importer()..importerId = 1 301 | ])); 302 | await _canonicalize(process); 303 | 304 | var request = getImportRequest(await process.outbound.next); 305 | process.inbound.add(InboundMessage() 306 | ..importResponse = (InboundMessage_ImportResponse() 307 | ..id = request.id 308 | ..error = "oh no")); 309 | 310 | var failure = getCompileFailure(await process.outbound.next); 311 | expect(failure.message, equals('oh no')); 312 | expect(failure.span.text, equals("'other'")); 313 | expect(failure.stackTrace, equals('- 1:9 root stylesheet\n')); 314 | await process.kill(); 315 | }); 316 | 317 | test("can return an SCSS file", () async { 318 | process.inbound.add(compileString("@import 'other'", importers: [ 319 | InboundMessage_CompileRequest_Importer()..importerId = 1 320 | ])); 321 | await _canonicalize(process); 322 | 323 | var request = getImportRequest(await process.outbound.next); 324 | process.inbound.add(InboundMessage() 325 | ..importResponse = (InboundMessage_ImportResponse() 326 | ..id = request.id 327 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 328 | ..contents = "a {b: 1px + 2px}"))); 329 | 330 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 331 | await process.kill(); 332 | }); 333 | 334 | test("can return an indented syntax file", () async { 335 | process.inbound.add(compileString("@import 'other'", importers: [ 336 | InboundMessage_CompileRequest_Importer()..importerId = 1 337 | ])); 338 | await _canonicalize(process); 339 | 340 | var request = getImportRequest(await process.outbound.next); 341 | process.inbound.add(InboundMessage() 342 | ..importResponse = (InboundMessage_ImportResponse() 343 | ..id = request.id 344 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 345 | ..contents = "a\n b: 1px + 2px" 346 | ..syntax = Syntax.INDENTED))); 347 | 348 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 349 | await process.kill(); 350 | }); 351 | 352 | test("can return a plain CSS file", () async { 353 | process.inbound.add(compileString("@import 'other'", importers: [ 354 | InboundMessage_CompileRequest_Importer()..importerId = 1 355 | ])); 356 | await _canonicalize(process); 357 | 358 | var request = getImportRequest(await process.outbound.next); 359 | process.inbound.add(InboundMessage() 360 | ..importResponse = (InboundMessage_ImportResponse() 361 | ..id = request.id 362 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 363 | ..contents = "a {b: c}" 364 | ..syntax = Syntax.CSS))); 365 | 366 | await expectLater(process.outbound, emits(isSuccess("a { b: c; }"))); 367 | await process.kill(); 368 | }); 369 | 370 | test("uses a data: URL rather than an empty source map URL", () async { 371 | process.inbound.add(compileString("@import 'other'", 372 | sourceMap: true, 373 | importers: [ 374 | InboundMessage_CompileRequest_Importer()..importerId = 1 375 | ])); 376 | await _canonicalize(process); 377 | 378 | var request = getImportRequest(await process.outbound.next); 379 | process.inbound.add(InboundMessage() 380 | ..importResponse = (InboundMessage_ImportResponse() 381 | ..id = request.id 382 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 383 | ..contents = "a {b: c}" 384 | ..sourceMapUrl = ""))); 385 | 386 | await expectLater( 387 | process.outbound, 388 | emits(isSuccess("a { b: c; }", sourceMap: (String map) { 389 | var mapping = source_maps.parse(map) as source_maps.SingleMapping; 390 | expect(mapping.urls, [startsWith("data:")]); 391 | }))); 392 | await process.kill(); 393 | }); 394 | 395 | test("uses a non-empty source map URL", () async { 396 | process.inbound.add(compileString("@import 'other'", 397 | sourceMap: true, 398 | importers: [ 399 | InboundMessage_CompileRequest_Importer()..importerId = 1 400 | ])); 401 | await _canonicalize(process); 402 | 403 | var request = getImportRequest(await process.outbound.next); 404 | process.inbound.add(InboundMessage() 405 | ..importResponse = (InboundMessage_ImportResponse() 406 | ..id = request.id 407 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 408 | ..contents = "a {b: c}" 409 | ..sourceMapUrl = "file:///asdf"))); 410 | 411 | await expectLater( 412 | process.outbound, 413 | emits(isSuccess("a { b: c; }", sourceMap: (String map) { 414 | var mapping = source_maps.parse(map) as source_maps.SingleMapping; 415 | expect(mapping.urls, equals(["file:///asdf"])); 416 | }))); 417 | await process.kill(); 418 | }); 419 | }); 420 | 421 | test("handles an importer for a string compile request", () async { 422 | process.inbound.add(compileString("@import 'other'", 423 | importer: InboundMessage_CompileRequest_Importer()..importerId = 1)); 424 | await _canonicalize(process); 425 | 426 | var request = getImportRequest(await process.outbound.next); 427 | process.inbound.add(InboundMessage() 428 | ..importResponse = (InboundMessage_ImportResponse() 429 | ..id = request.id 430 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 431 | ..contents = "a {b: 1px + 2px}"))); 432 | 433 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 434 | await process.kill(); 435 | }); 436 | 437 | group("load paths", () { 438 | test("are used to load imports", () async { 439 | await d.dir("dir", [d.file("other.scss", "a {b: c}")]).create(); 440 | 441 | process.inbound.add(compileString("@import 'other'", importers: [ 442 | InboundMessage_CompileRequest_Importer()..path = d.path("dir") 443 | ])); 444 | 445 | await expectLater(process.outbound, emits(isSuccess("a { b: c; }"))); 446 | await process.kill(); 447 | }); 448 | 449 | test("are accessed in order", () async { 450 | for (var i = 0; i < 3; i++) { 451 | await d.dir("dir$i", [d.file("other$i.scss", "a {b: $i}")]).create(); 452 | } 453 | 454 | process.inbound.add(compileString("@import 'other2'", importers: [ 455 | for (var i = 0; i < 3; i++) 456 | InboundMessage_CompileRequest_Importer()..path = d.path("dir$i") 457 | ])); 458 | 459 | await expectLater(process.outbound, emits(isSuccess("a { b: 2; }"))); 460 | await process.kill(); 461 | }); 462 | 463 | test("take precedence over later importers", () async { 464 | await d.dir("dir", [d.file("other.scss", "a {b: c}")]).create(); 465 | 466 | process.inbound.add(compileString("@import 'other'", importers: [ 467 | InboundMessage_CompileRequest_Importer()..path = d.path("dir"), 468 | InboundMessage_CompileRequest_Importer()..importerId = 1 469 | ])); 470 | 471 | await expectLater(process.outbound, emits(isSuccess("a { b: c; }"))); 472 | await process.kill(); 473 | }); 474 | 475 | test("yield precedence to earlier importers", () async { 476 | await d.dir("dir", [d.file("other.scss", "a {b: c}")]).create(); 477 | 478 | process.inbound.add(compileString("@import 'other'", importers: [ 479 | InboundMessage_CompileRequest_Importer()..importerId = 1, 480 | InboundMessage_CompileRequest_Importer()..path = d.path("dir") 481 | ])); 482 | await _canonicalize(process); 483 | 484 | var request = getImportRequest(await process.outbound.next); 485 | process.inbound.add(InboundMessage() 486 | ..importResponse = (InboundMessage_ImportResponse() 487 | ..id = request.id 488 | ..success = (InboundMessage_ImportResponse_ImportSuccess() 489 | ..contents = "x {y: z}"))); 490 | 491 | await expectLater(process.outbound, emits(isSuccess("x { y: z; }"))); 492 | await process.kill(); 493 | }); 494 | }); 495 | } 496 | 497 | /// Handles a `CanonicalizeRequest` and returns a response with a generic 498 | /// canonical URL. 499 | /// 500 | /// This is used when testing import requests, to avoid duplicating a bunch of 501 | /// generic code for canonicalization. It shouldn't be used for testing 502 | /// canonicalization itself. 503 | Future _canonicalize(EmbeddedProcess process) async { 504 | var request = getCanonicalizeRequest(await process.outbound.next); 505 | process.inbound.add(InboundMessage() 506 | ..canonicalizeResponse = (InboundMessage_CanonicalizeResponse() 507 | ..id = request.id 508 | ..url = "custom:other")); 509 | } 510 | 511 | /// Asserts that [process] emits a [CompileFailure] result with the given 512 | /// [message] on its protobuf stream and causes the compilation to fail. 513 | Future _expectImportError(EmbeddedProcess process, Object message) async { 514 | var failure = getCompileFailure(await process.outbound.next); 515 | expect(failure.message, equals(message)); 516 | expect(failure.span.text, equals("'other'")); 517 | } 518 | -------------------------------------------------------------------------------- /test/length_delimited_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:async'; 6 | import 'dart:typed_data'; 7 | 8 | import 'package:sass_embedded/src/util/length_delimited_transformer.dart'; 9 | 10 | import 'package:async/async.dart'; 11 | import 'package:test/test.dart'; 12 | 13 | void main() { 14 | group("encoder", () { 15 | late Sink> sink; 16 | late Stream> stream; 17 | setUp(() { 18 | var controller = StreamController>(); 19 | sink = controller.sink; 20 | stream = controller.stream 21 | .map((chunk) => Uint8List.fromList(chunk)) 22 | .transform(lengthDelimitedEncoder); 23 | }); 24 | 25 | test("encodes an empty message", () { 26 | sink.add([]); 27 | sink.close(); 28 | expect(collectBytes(stream), completion(equals([0]))); 29 | }); 30 | 31 | test("encodes a message of length 1", () { 32 | sink.add([123]); 33 | sink.close(); 34 | expect(collectBytes(stream), completion(equals([1, 123]))); 35 | }); 36 | 37 | test("encodes a message of length greater than 256", () { 38 | sink.add(List.filled(300, 1)); 39 | sink.close(); 40 | expect(collectBytes(stream), 41 | completion(equals([172, 2, ...List.filled(300, 1)]))); 42 | }); 43 | 44 | test("encodes multiple messages", () { 45 | sink.add([10]); 46 | sink.add([20, 30]); 47 | sink.add([40, 50, 60]); 48 | sink.close(); 49 | expect(collectBytes(stream), 50 | completion(equals([1, 10, 2, 20, 30, 3, 40, 50, 60]))); 51 | }); 52 | }); 53 | 54 | group("decoder", () { 55 | late Sink> sink; 56 | late StreamQueue queue; 57 | setUp(() { 58 | var controller = StreamController>(); 59 | sink = controller.sink; 60 | queue = StreamQueue(controller.stream.transform(lengthDelimitedDecoder)); 61 | }); 62 | 63 | group("decodes an empty message", () { 64 | test("from a single chunk", () { 65 | sink.add([0]); 66 | expect(queue, emits(isEmpty)); 67 | }); 68 | 69 | test("from a chunk that contains more data", () { 70 | sink.add([0, 1, 100]); 71 | expect(queue, emits(isEmpty)); 72 | }); 73 | }); 74 | 75 | group("decodes a longer message", () { 76 | test("from a single chunk", () { 77 | sink.add([172, 2, ...List.filled(300, 1)]); 78 | expect(queue, emits(List.filled(300, 1))); 79 | }); 80 | 81 | test("from multiple chunks", () { 82 | sink 83 | ..add([172]) 84 | ..add([2, 1]) 85 | ..add(List.filled(299, 1)); 86 | expect(queue, emits(List.filled(300, 1))); 87 | }); 88 | 89 | test("from one chunk per byte", () { 90 | for (var byte in [172, 2, ...List.filled(300, 1)]) { 91 | sink.add([byte]); 92 | } 93 | expect(queue, emits(List.filled(300, 1))); 94 | }); 95 | 96 | test("from a chunk that contains more data", () { 97 | sink.add([172, 2, ...List.filled(300, 1), 1, 10]); 98 | expect(queue, emits(List.filled(300, 1))); 99 | }); 100 | }); 101 | 102 | group("decodes multiple messages", () { 103 | test("from single chunk", () { 104 | sink.add([4, 1, 2, 3, 4, 2, 101, 102]); 105 | expect(queue, emits([1, 2, 3, 4])); 106 | expect(queue, emits([101, 102])); 107 | }); 108 | 109 | test("from multiple chunks", () { 110 | sink 111 | ..add([4]) 112 | ..add([1, 2, 3, 4, 172]) 113 | ..add([2, ...List.filled(300, 1)]); 114 | expect(queue, emits([1, 2, 3, 4])); 115 | expect(queue, emits(List.filled(300, 1))); 116 | }); 117 | 118 | test("from one chunk per byte", () { 119 | for (var byte in [4, 1, 2, 3, 4, 172, 2, ...List.filled(300, 1)]) { 120 | sink.add([byte]); 121 | } 122 | expect(queue, emits([1, 2, 3, 4])); 123 | expect(queue, emits(List.filled(300, 1))); 124 | }); 125 | }); 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /test/protocol_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:path/path.dart' as p; 6 | import 'package:pub_semver/pub_semver.dart'; 7 | import 'package:source_maps/source_maps.dart' as source_maps; 8 | import 'package:test/test.dart'; 9 | import 'package:test_descriptor/test_descriptor.dart' as d; 10 | 11 | import 'package:sass_embedded/src/embedded_sass.pb.dart'; 12 | 13 | import 'embedded_process.dart'; 14 | import 'utils.dart'; 15 | 16 | void main() { 17 | late EmbeddedProcess process; 18 | setUp(() async { 19 | process = await EmbeddedProcess.start(); 20 | }); 21 | 22 | group("exits upon protocol error", () { 23 | test("caused by an empty message", () async { 24 | process.inbound.add(InboundMessage()); 25 | await expectParseError(process, "InboundMessage.message is not set."); 26 | expect(await process.exitCode, 76); 27 | }); 28 | 29 | test("caused by an invalid message", () async { 30 | process.stdin.add([1, 0]); 31 | await expectParseError( 32 | process, "Protocol message contained an invalid tag (zero)."); 33 | expect(await process.exitCode, 76); 34 | }); 35 | }); 36 | 37 | test("a version response is valid", () async { 38 | process.inbound.add(InboundMessage() 39 | ..versionRequest = (InboundMessage_VersionRequest()..id = 123)); 40 | var response = (await process.outbound.next).versionResponse; 41 | expect(response.id, equals(123)); 42 | 43 | Version.parse(response.protocolVersion); // shouldn't throw 44 | Version.parse(response.compilerVersion); // shouldn't throw 45 | Version.parse(response.implementationVersion); // shouldn't throw 46 | expect(response.implementationName, equals("Dart Sass")); 47 | await process.kill(); 48 | }); 49 | 50 | group("compiles CSS from", () { 51 | test("an SCSS string by default", () async { 52 | process.inbound.add(compileString("a {b: 1px + 2px}")); 53 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 54 | await process.kill(); 55 | }); 56 | 57 | test("an SCSS string explicitly", () async { 58 | process.inbound 59 | .add(compileString("a {b: 1px + 2px}", syntax: Syntax.SCSS)); 60 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 61 | await process.kill(); 62 | }); 63 | 64 | test("an indented syntax string", () async { 65 | process.inbound 66 | .add(compileString("a\n b: 1px + 2px", syntax: Syntax.INDENTED)); 67 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 68 | await process.kill(); 69 | }); 70 | 71 | test("a plain CSS string", () async { 72 | process.inbound.add(compileString("a {b: c}", syntax: Syntax.CSS)); 73 | await expectLater(process.outbound, emits(isSuccess("a { b: c; }"))); 74 | await process.kill(); 75 | }); 76 | 77 | test("an absolute path", () async { 78 | await d.file("test.scss", "a {b: 1px + 2px}").create(); 79 | 80 | process.inbound.add(InboundMessage() 81 | ..compileRequest = (InboundMessage_CompileRequest() 82 | ..path = p.absolute(d.path("test.scss")))); 83 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 84 | await process.kill(); 85 | }); 86 | 87 | test("a relative path", () async { 88 | await d.file("test.scss", "a {b: 1px + 2px}").create(); 89 | 90 | process.inbound.add(InboundMessage() 91 | ..compileRequest = (InboundMessage_CompileRequest() 92 | ..path = p.relative(d.path("test.scss")))); 93 | await expectLater(process.outbound, emits(isSuccess("a { b: 3px; }"))); 94 | await process.kill(); 95 | }); 96 | }); 97 | 98 | group("compiles CSS in", () { 99 | test("expanded mode", () async { 100 | process.inbound 101 | .add(compileString("a {b: 1px + 2px}", style: OutputStyle.EXPANDED)); 102 | await expectLater( 103 | process.outbound, emits(isSuccess(equals("a {\n b: 3px;\n}")))); 104 | await process.kill(); 105 | }); 106 | 107 | test("compressed mode", () async { 108 | process.inbound.add( 109 | compileString("a {b: 1px + 2px}", style: OutputStyle.COMPRESSED)); 110 | await expectLater(process.outbound, emits(isSuccess(equals("a{b:3px}")))); 111 | await process.kill(); 112 | }); 113 | }); 114 | 115 | test("doesn't include a source map by default", () async { 116 | process.inbound.add(compileString("a {b: 1px + 2px}")); 117 | await expectLater(process.outbound, 118 | emits(isSuccess("a { b: 3px; }", sourceMap: isEmpty))); 119 | await process.kill(); 120 | }); 121 | 122 | test("doesn't include a source map with source_map: false", () async { 123 | process.inbound.add(compileString("a {b: 1px + 2px}", sourceMap: false)); 124 | await expectLater(process.outbound, 125 | emits(isSuccess("a { b: 3px; }", sourceMap: isEmpty))); 126 | await process.kill(); 127 | }); 128 | 129 | test("includes a source map if source_map is true", () async { 130 | process.inbound.add(compileString("a {b: 1px + 2px}", sourceMap: true)); 131 | await expectLater( 132 | process.outbound, 133 | emits(isSuccess("a { b: 3px; }", sourceMap: (String map) { 134 | var mapping = source_maps.parse(map); 135 | var span = mapping.spanFor(2, 5)!; 136 | expect(span.start.line, equals(0)); 137 | expect(span.start.column, equals(3)); 138 | expect(span.end, equals(span.start)); 139 | expect(mapping, isA()); 140 | expect((mapping as source_maps.SingleMapping).files[0], isNull); 141 | return true; 142 | }))); 143 | await process.kill(); 144 | }); 145 | 146 | test( 147 | "includes a source map without content if source_map is true and source_map_include_sources is false", 148 | () async { 149 | process.inbound.add(compileString("a {b: 1px + 2px}", 150 | sourceMap: true, sourceMapIncludeSources: false)); 151 | await expectLater( 152 | process.outbound, 153 | emits(isSuccess("a { b: 3px; }", sourceMap: (String map) { 154 | var mapping = source_maps.parse(map); 155 | var span = mapping.spanFor(2, 5)!; 156 | expect(span.start.line, equals(0)); 157 | expect(span.start.column, equals(3)); 158 | expect(span.end, equals(span.start)); 159 | expect(mapping, isA()); 160 | expect((mapping as source_maps.SingleMapping).files[0], isNull); 161 | return true; 162 | }))); 163 | await process.kill(); 164 | }); 165 | 166 | test( 167 | "includes a source map with content if source_map is true and source_map_include_sources is true", 168 | () async { 169 | process.inbound.add(compileString("a {b: 1px + 2px}", 170 | sourceMap: true, sourceMapIncludeSources: true)); 171 | await expectLater( 172 | process.outbound, 173 | emits(isSuccess("a { b: 3px; }", sourceMap: (String map) { 174 | var mapping = source_maps.parse(map); 175 | var span = mapping.spanFor(2, 5)!; 176 | expect(span.start.line, equals(0)); 177 | expect(span.start.column, equals(3)); 178 | expect(span.end, equals(span.start)); 179 | expect(mapping, isA()); 180 | expect((mapping as source_maps.SingleMapping).files[0], isNotNull); 181 | return true; 182 | }))); 183 | await process.kill(); 184 | }); 185 | 186 | group("emits a log event", () { 187 | group("for a @debug rule", () { 188 | test("with correct fields", () async { 189 | process.inbound.add(compileString("a {@debug hello}")); 190 | 191 | var logEvent = getLogEvent(await process.outbound.next); 192 | expect(logEvent.compilationId, equals(0)); 193 | expect(logEvent.type, equals(LogEventType.DEBUG)); 194 | expect(logEvent.message, equals("hello")); 195 | expect(logEvent.span.text, equals("@debug hello")); 196 | expect(logEvent.span.start, equals(location(3, 0, 3))); 197 | expect(logEvent.span.end, equals(location(15, 0, 15))); 198 | expect(logEvent.span.context, equals("a {@debug hello}")); 199 | expect(logEvent.stackTrace, isEmpty); 200 | expect(logEvent.formatted, equals('-:1 DEBUG: hello\n')); 201 | await process.kill(); 202 | }); 203 | 204 | test("formatted with terminal colors", () async { 205 | process.inbound 206 | .add(compileString("a {@debug hello}", alertColor: true)); 207 | var logEvent = getLogEvent(await process.outbound.next); 208 | expect( 209 | logEvent.formatted, equals('-:1 \u001b[1mDebug\u001b[0m: hello\n')); 210 | await process.kill(); 211 | }); 212 | }); 213 | 214 | group("for a @warn rule", () { 215 | test("with correct fields", () async { 216 | process.inbound.add(compileString("a {@warn hello}")); 217 | 218 | var logEvent = getLogEvent(await process.outbound.next); 219 | expect(logEvent.compilationId, equals(0)); 220 | expect(logEvent.type, equals(LogEventType.WARNING)); 221 | expect(logEvent.message, equals("hello")); 222 | expect(logEvent.span, equals(SourceSpan())); 223 | expect(logEvent.stackTrace, equals("- 1:4 root stylesheet\n")); 224 | expect( 225 | logEvent.formatted, 226 | equals('WARNING: hello\n' 227 | ' - 1:4 root stylesheet\n')); 228 | await process.kill(); 229 | }); 230 | 231 | test("formatted with terminal colors", () async { 232 | process.inbound.add(compileString("a {@warn hello}", alertColor: true)); 233 | var logEvent = getLogEvent(await process.outbound.next); 234 | expect( 235 | logEvent.formatted, 236 | equals('\x1B[33m\x1B[1mWarning\x1B[0m: hello\n' 237 | ' - 1:4 root stylesheet\n')); 238 | await process.kill(); 239 | }); 240 | 241 | test("encoded in ASCII", () async { 242 | process.inbound 243 | .add(compileString("a {@debug a && b}", alertAscii: true)); 244 | var logEvent = getLogEvent(await process.outbound.next); 245 | expect( 246 | logEvent.formatted, 247 | equals('WARNING on line 1, column 13: \n' 248 | 'In Sass, "&&" means two copies of the parent selector. You probably want to use "and" instead.\n' 249 | ' ,\n' 250 | '1 | a {@debug a && b}\n' 251 | ' | ^^\n' 252 | ' \'\n')); 253 | await process.kill(); 254 | }); 255 | }); 256 | 257 | test("for a parse-time deprecation warning", () async { 258 | process.inbound.add(compileString("@if true {} @elseif true {}")); 259 | 260 | var logEvent = getLogEvent(await process.outbound.next); 261 | expect(logEvent.compilationId, equals(0)); 262 | expect(logEvent.type, equals(LogEventType.DEPRECATION_WARNING)); 263 | expect( 264 | logEvent.message, 265 | equals( 266 | '@elseif is deprecated and will not be supported in future Sass ' 267 | 'versions.\n' 268 | '\n' 269 | 'Recommendation: @else if')); 270 | expect(logEvent.span.text, equals("@elseif")); 271 | expect(logEvent.span.start, equals(location(12, 0, 12))); 272 | expect(logEvent.span.end, equals(location(19, 0, 19))); 273 | expect(logEvent.span.context, equals("@if true {} @elseif true {}")); 274 | expect(logEvent.stackTrace, isEmpty); 275 | await process.kill(); 276 | }); 277 | 278 | test("for a runtime deprecation warning", () async { 279 | process.inbound.add(compileString("a {\$var: value !global}")); 280 | 281 | var logEvent = getLogEvent(await process.outbound.next); 282 | expect(logEvent.compilationId, equals(0)); 283 | expect(logEvent.type, equals(LogEventType.DEPRECATION_WARNING)); 284 | expect( 285 | logEvent.message, 286 | equals("As of Dart Sass 2.0.0, !global assignments won't be able to " 287 | "declare new variables.\n" 288 | "\n" 289 | "Recommendation: add `\$var: null` at the stylesheet root.")); 290 | expect(logEvent.span.text, equals("\$var: value !global")); 291 | expect(logEvent.span.start, equals(location(3, 0, 3))); 292 | expect(logEvent.span.end, equals(location(22, 0, 22))); 293 | expect(logEvent.span.context, equals("a {\$var: value !global}")); 294 | expect(logEvent.stackTrace, "- 1:4 root stylesheet\n"); 295 | await process.kill(); 296 | }); 297 | 298 | test("with the same ID as the CompileRequest", () async { 299 | process.inbound.add(compileString("@debug hello", id: 12345)); 300 | 301 | var logEvent = getLogEvent(await process.outbound.next); 302 | expect(logEvent.compilationId, equals(12345)); 303 | await process.kill(); 304 | }); 305 | }); 306 | 307 | group("gracefully handles an error", () { 308 | test("from invalid syntax", () async { 309 | process.inbound.add(compileString("a {b: }")); 310 | 311 | var failure = getCompileFailure(await process.outbound.next); 312 | expect(failure.message, equals("Expected expression.")); 313 | expect(failure.span.text, isEmpty); 314 | expect(failure.span.start, equals(location(6, 0, 6))); 315 | expect(failure.span.end, equals(location(6, 0, 6))); 316 | expect(failure.span.url, isEmpty); 317 | expect(failure.span.context, equals("a {b: }")); 318 | expect(failure.stackTrace, equals("- 1:7 root stylesheet\n")); 319 | await process.kill(); 320 | }); 321 | 322 | test("from the runtime", () async { 323 | process.inbound.add(compileString("a {b: 1px + 1em}")); 324 | 325 | var failure = getCompileFailure(await process.outbound.next); 326 | expect(failure.message, equals("1px and 1em have incompatible units.")); 327 | expect(failure.span.text, "1px + 1em"); 328 | expect(failure.span.start, equals(location(6, 0, 6))); 329 | expect(failure.span.end, equals(location(15, 0, 15))); 330 | expect(failure.span.url, isEmpty); 331 | expect(failure.span.context, equals("a {b: 1px + 1em}")); 332 | expect(failure.stackTrace, equals("- 1:7 root stylesheet\n")); 333 | await process.kill(); 334 | }); 335 | 336 | test("from a missing file", () async { 337 | process.inbound.add(InboundMessage() 338 | ..compileRequest = 339 | (InboundMessage_CompileRequest()..path = d.path("test.scss"))); 340 | 341 | var failure = getCompileFailure(await process.outbound.next); 342 | expect(failure.message, startsWith("Cannot open file: ")); 343 | expect(failure.message.replaceFirst("Cannot open file: ", "").trim(), 344 | equalsPath(d.path('test.scss'))); 345 | expect(failure.span.text, equals('')); 346 | expect(failure.span.context, equals('')); 347 | expect(failure.span.start, equals(SourceSpan_SourceLocation())); 348 | expect(failure.span.end, equals(SourceSpan_SourceLocation())); 349 | expect(failure.span.url, equals(p.toUri(d.path('test.scss')).toString())); 350 | expect(failure.stackTrace, isEmpty); 351 | await process.kill(); 352 | }); 353 | 354 | test("with a multi-line source span", () async { 355 | process.inbound.add(compileString(""" 356 | a { 357 | b: 1px + 358 | 1em; 359 | } 360 | """)); 361 | 362 | var failure = getCompileFailure(await process.outbound.next); 363 | expect(failure.span.text, "1px +\n 1em"); 364 | expect(failure.span.start, equals(location(9, 1, 5))); 365 | expect(failure.span.end, equals(location(23, 2, 8))); 366 | expect(failure.span.url, isEmpty); 367 | expect(failure.span.context, equals(" b: 1px +\n 1em;\n")); 368 | expect(failure.stackTrace, equals("- 2:6 root stylesheet\n")); 369 | await process.kill(); 370 | }); 371 | 372 | test("with multiple stack trace entries", () async { 373 | process.inbound.add(compileString(""" 374 | @function fail() { 375 | @return 1px + 1em; 376 | } 377 | 378 | a { 379 | b: fail(); 380 | } 381 | """)); 382 | 383 | var failure = getCompileFailure(await process.outbound.next); 384 | expect( 385 | failure.stackTrace, 386 | equals("- 2:11 fail()\n" 387 | "- 6:6 root stylesheet\n")); 388 | await process.kill(); 389 | }); 390 | 391 | group("and includes the URL from", () { 392 | test("a string input", () async { 393 | process.inbound 394 | .add(compileString("a {b: 1px + 1em}", url: "foo://bar/baz")); 395 | 396 | var failure = getCompileFailure(await process.outbound.next); 397 | expect(failure.span.url, equals("foo://bar/baz")); 398 | expect( 399 | failure.stackTrace, equals("foo://bar/baz 1:7 root stylesheet\n")); 400 | await process.kill(); 401 | }); 402 | 403 | test("a path input", () async { 404 | await d.file("test.scss", "a {b: 1px + 1em}").create(); 405 | var path = d.path("test.scss"); 406 | process.inbound.add(InboundMessage() 407 | ..compileRequest = (InboundMessage_CompileRequest()..path = path)); 408 | 409 | var failure = getCompileFailure(await process.outbound.next); 410 | expect(p.fromUri(failure.span.url), equalsPath(path)); 411 | expect(failure.stackTrace, endsWith(" 1:7 root stylesheet\n")); 412 | expect(failure.stackTrace.split(" ").first, equalsPath(path)); 413 | await process.kill(); 414 | }); 415 | }); 416 | 417 | test("caused by using Sass features in CSS", () async { 418 | process.inbound 419 | .add(compileString("a {b: 1px + 2px}", syntax: Syntax.CSS)); 420 | 421 | var failure = getCompileFailure(await process.outbound.next); 422 | expect(failure.message, equals("Operators aren't allowed in plain CSS.")); 423 | expect(failure.span.text, "+"); 424 | expect(failure.span.start, equals(location(10, 0, 10))); 425 | expect(failure.span.end, equals(location(11, 0, 11))); 426 | expect(failure.span.url, isEmpty); 427 | expect(failure.span.context, equals("a {b: 1px + 2px}")); 428 | expect(failure.stackTrace, equals("- 1:11 root stylesheet\n")); 429 | await process.kill(); 430 | }); 431 | 432 | group("and provides a formatted", () { 433 | test("message", () async { 434 | process.inbound.add(compileString("a {b: 1px + 1em}")); 435 | 436 | var failure = getCompileFailure(await process.outbound.next); 437 | expect( 438 | failure.formatted, 439 | equals('Error: 1px and 1em have incompatible units.\n' 440 | ' ╷\n' 441 | '1 │ a {b: 1px + 1em}\n' 442 | ' │ ^^^^^^^^^\n' 443 | ' ╵\n' 444 | ' - 1:7 root stylesheet')); 445 | await process.kill(); 446 | }); 447 | 448 | test("message with terminal colors", () async { 449 | process.inbound 450 | .add(compileString("a {b: 1px + 1em}", alertColor: true)); 451 | 452 | var failure = getCompileFailure(await process.outbound.next); 453 | expect( 454 | failure.formatted, 455 | equals('Error: 1px and 1em have incompatible units.\n' 456 | '\x1B[34m ╷\x1B[0m\n' 457 | '\x1B[34m1 │\x1B[0m a {b: \x1B[31m1px + 1em\x1B[0m}\n' 458 | '\x1B[34m │\x1B[0m \x1B[31m ^^^^^^^^^\x1B[0m\n' 459 | '\x1B[34m ╵\x1B[0m\n' 460 | ' - 1:7 root stylesheet')); 461 | await process.kill(); 462 | }); 463 | 464 | test("message with ASCII encoding", () async { 465 | process.inbound 466 | .add(compileString("a {b: 1px + 1em}", alertAscii: true)); 467 | 468 | var failure = getCompileFailure(await process.outbound.next); 469 | expect( 470 | failure.formatted, 471 | equals('Error: 1px and 1em have incompatible units.\n' 472 | ' ,\n' 473 | '1 | a {b: 1px + 1em}\n' 474 | ' | ^^^^^^^^^\n' 475 | ' \'\n' 476 | ' - 1:7 root stylesheet')); 477 | await process.kill(); 478 | }); 479 | }); 480 | }); 481 | } 482 | -------------------------------------------------------------------------------- /test/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'package:path/path.dart' as p; 6 | import 'package:test/test.dart'; 7 | 8 | import 'package:sass_embedded/src/embedded_sass.pb.dart'; 9 | import 'package:sass_embedded/src/utils.dart'; 10 | 11 | import 'embedded_process.dart'; 12 | 13 | /// Returns a [InboundMessage] that compiles the given plain CSS 14 | /// string. 15 | InboundMessage compileString(String css, 16 | {int? id, 17 | bool? alertColor, 18 | bool? alertAscii, 19 | Syntax? syntax, 20 | OutputStyle? style, 21 | String? url, 22 | bool? sourceMap, 23 | bool? sourceMapIncludeSources, 24 | Iterable? importers, 25 | InboundMessage_CompileRequest_Importer? importer, 26 | Iterable? functions}) { 27 | var input = InboundMessage_CompileRequest_StringInput()..source = css; 28 | if (syntax != null) input.syntax = syntax; 29 | if (url != null) input.url = url; 30 | if (importer != null) input.importer = importer; 31 | 32 | var request = InboundMessage_CompileRequest()..string = input; 33 | if (id != null) request.id = id; 34 | if (importers != null) request.importers.addAll(importers); 35 | if (style != null) request.style = style; 36 | if (sourceMap != null) request.sourceMap = sourceMap; 37 | if (sourceMapIncludeSources != null) { 38 | request.sourceMapIncludeSources = sourceMapIncludeSources; 39 | } 40 | if (functions != null) request.globalFunctions.addAll(functions); 41 | if (alertColor != null) request.alertColor = alertColor; 42 | if (alertAscii != null) request.alertAscii = alertAscii; 43 | 44 | return InboundMessage()..compileRequest = request; 45 | } 46 | 47 | /// Asserts that [process] emits a [ProtocolError] parse error with the given 48 | /// [message] on its protobuf stream and prints a notice on stderr. 49 | Future expectParseError(EmbeddedProcess process, Object message) async { 50 | await expectLater(process.outbound, 51 | emits(isProtocolError(errorId, ProtocolErrorType.PARSE, message))); 52 | 53 | var stderrPrefix = "Host caused parse error: "; 54 | await expectLater( 55 | process.stderr, 56 | message is String 57 | ? emitsInOrder("$stderrPrefix$message".split("\n")) 58 | : emits(startsWith(stderrPrefix))); 59 | } 60 | 61 | /// Asserts that [process] emits a [ProtocolError] params error with the given 62 | /// [message] on its protobuf stream and prints a notice on stderr. 63 | Future expectParamsError( 64 | EmbeddedProcess process, int id, Object message) async { 65 | await expectLater(process.outbound, 66 | emits(isProtocolError(id, ProtocolErrorType.PARAMS, message))); 67 | 68 | var stderrPrefix = "Host caused params error" 69 | "${id == errorId ? '' : " with request $id"}: "; 70 | await expectLater( 71 | process.stderr, 72 | message is String 73 | ? emitsInOrder("$stderrPrefix$message".split("\n")) 74 | : emits(startsWith(stderrPrefix))); 75 | } 76 | 77 | /// Asserts that an [OutboundMessage] is a [ProtocolError] with the given [id], 78 | /// [type], and optionally [message]. 79 | Matcher isProtocolError(int id, ProtocolErrorType type, [Object? message]) => 80 | predicate((value) { 81 | expect(value, isA()); 82 | var outboundMessage = value as OutboundMessage; 83 | expect(outboundMessage.hasError(), isTrue, 84 | reason: "Expected $outboundMessage to be a ProtocolError"); 85 | expect(outboundMessage.error.id, equals(id)); 86 | expect(outboundMessage.error.type, equals(type)); 87 | if (message != null) expect(outboundMessage.error.message, message); 88 | return true; 89 | }); 90 | 91 | /// Asserts that [message] is an [OutboundMessage] with a 92 | /// `CanonicalizeRequest` and returns it. 93 | OutboundMessage_CanonicalizeRequest getCanonicalizeRequest(Object? value) { 94 | expect(value, isA()); 95 | var message = value as OutboundMessage; 96 | expect(message.hasCanonicalizeRequest(), isTrue, 97 | reason: "Expected $message to have a CanonicalizeRequest"); 98 | return message.canonicalizeRequest; 99 | } 100 | 101 | /// Asserts that [message] is an [OutboundMessage] with a `ImportRequest` and 102 | /// returns it. 103 | OutboundMessage_ImportRequest getImportRequest(Object? value) { 104 | expect(value, isA()); 105 | var message = value as OutboundMessage; 106 | expect(message.hasImportRequest(), isTrue, 107 | reason: "Expected $message to have a ImportRequest"); 108 | return message.importRequest; 109 | } 110 | 111 | /// Asserts that [message] is an [OutboundMessage] with a `FileImportRequest` 112 | /// and returns it. 113 | OutboundMessage_FileImportRequest getFileImportRequest(Object? value) { 114 | expect(value, isA()); 115 | var message = value as OutboundMessage; 116 | expect(message.hasFileImportRequest(), isTrue, 117 | reason: "Expected $message to have a FileImportRequest"); 118 | return message.fileImportRequest; 119 | } 120 | 121 | /// Asserts that [message] is an [OutboundMessage] with a 122 | /// `FunctionCallRequest` and returns it. 123 | OutboundMessage_FunctionCallRequest getFunctionCallRequest(Object? value) { 124 | expect(value, isA()); 125 | var message = value as OutboundMessage; 126 | expect(message.hasFunctionCallRequest(), isTrue, 127 | reason: "Expected $message to have a FunctionCallRequest"); 128 | return message.functionCallRequest; 129 | } 130 | 131 | /// Asserts that [message] is an [OutboundMessage] with a 132 | /// `CompileResponse.Failure` and returns it. 133 | OutboundMessage_CompileResponse_CompileFailure getCompileFailure( 134 | Object? value) { 135 | var response = getCompileResponse(value); 136 | expect(response.hasFailure(), isTrue, 137 | reason: "Expected $response to be a failure"); 138 | return response.failure; 139 | } 140 | 141 | /// Asserts that [message] is an [OutboundMessage] with a 142 | /// `CompileResponse.Success` and returns it. 143 | OutboundMessage_CompileResponse_CompileSuccess getCompileSuccess( 144 | Object? value) { 145 | var response = getCompileResponse(value); 146 | expect(response.hasSuccess(), isTrue, 147 | reason: "Expected $response to be a success"); 148 | return response.success; 149 | } 150 | 151 | /// Asserts that [message] is an [OutboundMessage] with a `CompileResponse` and 152 | /// returns it. 153 | OutboundMessage_CompileResponse getCompileResponse(Object? value) { 154 | expect(value, isA()); 155 | var message = value as OutboundMessage; 156 | expect(message.hasCompileResponse(), isTrue, 157 | reason: "Expected $message to have a CompileResponse"); 158 | return message.compileResponse; 159 | } 160 | 161 | /// Asserts that [message] is an [OutboundMessage] with a `LogEvent` and 162 | /// returns it. 163 | OutboundMessage_LogEvent getLogEvent(Object? value) { 164 | expect(value, isA()); 165 | var message = value as OutboundMessage; 166 | expect(message.hasLogEvent(), isTrue, 167 | reason: "Expected $message to have a LogEvent"); 168 | return message.logEvent; 169 | } 170 | 171 | /// Asserts that an [OutboundMessage] is a `CompileResponse` with CSS that 172 | /// matches [css], with a source map that matches [sourceMap] (if passed). 173 | /// 174 | /// If [css] is a [String], this automatically wraps it in 175 | /// [equalsIgnoringWhitespace]. 176 | /// 177 | /// If [sourceMap] is a function, `response.success.sourceMap` is passed to it. 178 | /// Otherwise, it's treated as a matcher for `response.success.sourceMap`. 179 | Matcher isSuccess(Object css, {Object? sourceMap}) => predicate((value) { 180 | var success = getCompileSuccess(value); 181 | expect(success.css, css is String ? equalsIgnoringWhitespace(css) : css); 182 | if (sourceMap is void Function(String)) { 183 | sourceMap(success.sourceMap); 184 | } else if (sourceMap != null) { 185 | expect(success.sourceMap, sourceMap); 186 | } 187 | return true; 188 | }); 189 | 190 | /// Returns a [SourceSpan_SourceLocation] with the given [offset], [line], and 191 | /// [column]. 192 | SourceSpan_SourceLocation location(int offset, int line, int column) => 193 | SourceSpan_SourceLocation() 194 | ..offset = offset 195 | ..line = line 196 | ..column = column; 197 | 198 | /// Returns a matcher that verifies whether the given value refers to the same 199 | /// path as [expected]. 200 | Matcher equalsPath(String expected) => predicate( 201 | (actual) => p.equals(actual, expected), "equals $expected"); 202 | -------------------------------------------------------------------------------- /tool/grind.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:cli_pkg/cli_pkg.dart' as pkg; 8 | import 'package:grinder/grinder.dart'; 9 | import 'package:yaml/yaml.dart'; 10 | 11 | import 'utils.dart'; 12 | 13 | void main(List args) { 14 | pkg.humanName.value = "Dart Sass Embedded"; 15 | pkg.botName.value = "Sass Bot"; 16 | pkg.botEmail.value = "sass.bot.beep.boop@gmail.com"; 17 | pkg.homebrewRepo.value = "sass/homebrew-sass"; 18 | pkg.homebrewFormula.value = "dart-sass-embedded.rb"; 19 | 20 | pkg.githubBearerToken.fn = () => Platform.environment["GH_BEARER_TOKEN"]!; 21 | pkg.githubUser.fn = () => Platform.environment["GH_USER"]; 22 | pkg.githubPassword.fn = () => Platform.environment["GH_TOKEN"]; 23 | 24 | pkg.environmentConstants.fn = () => { 25 | ...pkg.environmentConstants.defaultValue, 26 | "protocol-version": 27 | File('build/embedded-protocol/VERSION').readAsStringSync().trim(), 28 | "compiler-version": pkg.pubspec.version!.toString(), 29 | "implementation-version": _implementationVersion 30 | }; 31 | 32 | pkg.addGithubTasks(); 33 | pkg.addHomebrewTasks(); 34 | grind(args); 35 | } 36 | 37 | /// Returns the version of Dart Sass that this package uses. 38 | String get _implementationVersion { 39 | var lockfile = loadYaml(File('pubspec.lock').readAsStringSync(), 40 | sourceUrl: Uri(path: 'pubspec.lock')); 41 | return lockfile['packages']['sass']['version'] as String; 42 | } 43 | 44 | @Task('Compile the protocol buffer definition to a Dart library.') 45 | Future protobuf() async { 46 | Directory('build').createSync(recursive: true); 47 | 48 | // Make sure we use the version of protoc_plugin defined by our pubspec, 49 | // rather than whatever version the developer might have globally installed. 50 | log("Writing protoc-gen-dart"); 51 | if (Platform.isWindows) { 52 | File('build/protoc-gen-dart.bat').writeAsStringSync(''' 53 | @echo off 54 | dart run protoc_plugin %* 55 | '''); 56 | } else { 57 | File('build/protoc-gen-dart').writeAsStringSync(''' 58 | #!/bin/sh 59 | dart run protoc_plugin "\$@" 60 | '''); 61 | run('chmod', arguments: ['a+x', 'build/protoc-gen-dart']); 62 | } 63 | 64 | if (Platform.environment['UPDATE_SASS_PROTOCOL'] != 'false') { 65 | await cloneOrPull("https://github.com/sass/embedded-protocol.git"); 66 | } 67 | 68 | await runAsync("buf", 69 | arguments: ["generate"], 70 | runOptions: RunOptions(environment: { 71 | "PATH": 'build' + 72 | (Platform.isWindows ? ";" : ":") + 73 | Platform.environment["PATH"]! 74 | })); 75 | } 76 | -------------------------------------------------------------------------------- /tool/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:grinder/grinder.dart'; 8 | import 'package:path/path.dart' as p; 9 | 10 | /// Ensure that the repository at [url] is cloned into the build directory and 11 | /// pointing to the latest master revision. 12 | /// 13 | /// Returns the path to the repository. 14 | Future cloneOrPull(String url) async => 15 | cloneOrCheckout(url, "origin/main"); 16 | 17 | /// Ensure that the repository at [url] is cloned into the build directory and 18 | /// pointing to [ref]. 19 | /// 20 | /// Returns the path to the repository. 21 | Future cloneOrCheckout(String url, String ref) async { 22 | var name = p.url.basename(url); 23 | if (p.url.extension(name) == ".git") name = p.url.withoutExtension(name); 24 | 25 | var path = p.join("build", name); 26 | 27 | if (Directory(p.join(path, '.git')).existsSync()) { 28 | log("Updating $url"); 29 | await runAsync("git", 30 | arguments: ["fetch", "origin"], workingDirectory: path); 31 | } else { 32 | delete(Directory(path)); 33 | await runAsync("git", arguments: ["clone", url, path]); 34 | await runAsync("git", 35 | arguments: ["config", "advice.detachedHead", "false"], 36 | workingDirectory: path); 37 | } 38 | await runAsync("git", arguments: ["checkout", ref], workingDirectory: path); 39 | log(""); 40 | 41 | return path; 42 | } 43 | --------------------------------------------------------------------------------