├── .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 |
--------------------------------------------------------------------------------