diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 44ca4f044..c181c1c92 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,6 +5,17 @@ updates:
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
- directory: "/"
+ directories:
+ - "/"
+ - "/.github/util/*/"
+ schedule:
+ interval: "weekly"
+ - package-ecosystem: "npm"
+ directories:
+ - "/"
+ - "/package"
+ - "/pkg/sass-parser"
+ ignore:
+ - dependency-name: "sass"
schedule:
interval: "weekly"
diff --git a/.github/util/initialize/action.yml b/.github/util/initialize/action.yml
index ad2abda5e..2fba83ffa 100644
--- a/.github/util/initialize/action.yml
+++ b/.github/util/initialize/action.yml
@@ -2,7 +2,7 @@ name: Initialize
description: Check out Dart Sass and build the embedded protocol buffer.
inputs:
github-token: {required: true}
- node-version: {required: false, default: 18}
+ node-version: {required: false, default: 'lts/*'}
dart-sdk: {required: false, default: stable}
architecture: {required: false}
runs:
@@ -13,24 +13,42 @@ runs:
sdk: "${{ inputs.dart-sdk }}"
architecture: "${{ inputs.architecture }}"
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: "${{ inputs.node-version }}"
+ # See: https://github.com/dart-lang/sdk/issues/52266
+ - run: Invoke-WebRequest https://pub.dev
+ if: runner.os == 'Windows'
+ shell: powershell
+
+ # See: https://github.com/orgs/community/discussions/131594
+ # The composite action requires an explict shell, but bash is not available on windows-arm64 runner.
+ # For the following commands conditionally use bash or powershell based on the runner.os:
- run: dart pub get
- shell: bash
+ shell: ${{ runner.os == 'Windows' && 'powershell' || 'bash' }}
- run: npm install
- shell: bash
+ shell: ${{ runner.os == 'Windows' && 'powershell' || 'bash' }}
- - uses: bufbuild/buf-setup-action@v1.13.1
+ - uses: bufbuild/buf-setup-action@v1.46.0
with: {github_token: "${{ inputs.github-token }}"}
+ # This composite action requires bash, but bash is not available on windows-arm64 runner.
+ # Avoid running this composite action on non-PR, so that we can release on windows-arm64.
- name: Check out the language repo
+ if: github.event_name == 'pull_request'
uses: sass/clone-linked-repo@v1
with: {repo: sass/sass, path: build/language}
+ # Git is not pre-installed on windows-arm64 runner, however actions/checkout support
+ # downloading repo via GitHub API.
+ - name: Check out the language repo
+ if: github.event_name != 'pull_request'
+ uses: actions/checkout@v4
+ with: {repository: sass/sass, path: build/language}
+
- name: Generate Dart from protobuf
run: dart run grinder protobuf
- env: {UPDATE_SASS_PROTOCOL: false}
- shell: bash
+ env: {UPDATE_SASS_SASS_REPO: false}
+ shell: ${{ runner.os == 'Windows' && 'powershell' || 'bash' }}
diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml
new file mode 100644
index 000000000..4aa12a3bc
--- /dev/null
+++ b/.github/workflows/build-linux.yml
@@ -0,0 +1,136 @@
+name: Build for linux
+
+on:
+ workflow_call:
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: Build
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - image: docker.io/library/dart
+ platform: linux/amd64
+ target: linux-x64
+ - image: docker.io/library/dart
+ platform: linux/amd64
+ target: linux-ia32
+ - image: docker.io/library/dart
+ platform: linux/arm64
+ target: linux-arm64
+ - image: docker.io/library/dart
+ platform: linux/arm/v7
+ target: linux-arm
+ - image: docker.io/library/debian:unstable-slim
+ platform: linux/riscv64
+ target: linux-riscv64
+ - image: ghcr.io/dart-musl/dart
+ platform: linux/amd64
+ target: linux-x64-musl
+ - image: ghcr.io/dart-musl/dart
+ platform: linux/amd64
+ target: linux-ia32-musl
+ - image: ghcr.io/dart-musl/dart
+ platform: linux/arm64
+ target: linux-arm64-musl
+ - image: ghcr.io/dart-musl/dart
+ platform: linux/arm/v7
+ target: linux-arm-musl
+ - image: ghcr.io/dart-musl/dart
+ platform: linux/riscv64
+ target: linux-riscv64-musl
+ - image: ghcr.io/dart-android/dart
+ platform: linux/amd64
+ target: android-x64
+ - image: ghcr.io/dart-android/dart
+ platform: linux/amd64
+ target: android-ia32
+ - image: ghcr.io/dart-android/dart
+ platform: linux/arm64
+ target: android-arm64
+ - image: ghcr.io/dart-android/dart
+ platform: linux/arm64
+ target: android-arm
+ - image: ghcr.io/dart-android/dart
+ platform: linux/riscv64
+ target: android-riscv64
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Set up QEMU
+ run: docker run --privileged --rm registry.fedoraproject.org/fedora-minimal /bin/sh -c "microdnf install --assumeyes --nodocs --setopt=install_weak_deps=False qemu-user-static systemd-udev && mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && /usr/lib/systemd/systemd-binfmt --unregister && /usr/lib/systemd/systemd-binfmt"
+
+ - name: Build
+ if: matrix.image != 'ghcr.io/dart-android/dart' && matrix.image != 'docker.io/library/debian:unstable-slim'
+ run: |
+ docker run --rm -i \
+ --platform ${{ matrix.platform }} \
+ --volume "$PWD:$PWD" \
+ --workdir "$PWD" \
+ ${{ matrix.image }} <<'EOF'
+ set -e
+ dart pub get
+ dart run grinder pkg-standalone-${{ matrix.target }}
+ EOF
+
+ - name: Build
+ if: matrix.image == 'ghcr.io/dart-android/dart'
+ run: |
+ docker run --rm -i \
+ --privileged \
+ --platform ${{ matrix.platform }} \
+ --volume "$PWD:$PWD" \
+ --workdir "$PWD" \
+ ${{ matrix.image }} <<'EOF'
+ set -e
+ export DART_SDK=/system/${{ endsWith(matrix.target, '64') && 'lib64' || 'lib' }}/dart
+ export PATH=$DART_SDK/bin:$PATH
+ dart pub get
+ dart run grinder pkg-standalone-${{ matrix.target }}
+ EOF
+
+ # https://github.com/dart-lang/dart-docker/issues/96#issuecomment-1669860829
+ # There is no official riscv64 dart container image yet, build on debian:unstable instead.
+ # The setup is adopted from: https://github.com/dart-lang/dart-docker/blob/main/Dockerfile-debian.template
+ - name: Build
+ if: matrix.image == 'docker.io/library/debian:unstable-slim'
+ run: |
+ DART_CHANNEL=stable
+ DART_VERSION=$(curl -fsSL https://storage.googleapis.com/dart-archive/channels/$DART_CHANNEL/release/latest/VERSION | yq .version)
+ curl -fsSLO "https://storage.googleapis.com/dart-archive/channels/$DART_CHANNEL/release/$DART_VERSION/sdk/dartsdk-${{ matrix.target }}-release.zip"
+
+ docker run --rm -i \
+ --platform ${{ matrix.platform }} \
+ --volume "$PWD:$PWD" \
+ --workdir "$PWD" \
+ ${{ matrix.image }} <<'EOF'
+ set -e
+ apt-get update
+ apt-get install -y --no-install-recommends ca-certificates curl dnsutils git openssh-client unzip
+
+ export DART_SDK=/usr/lib/dart
+ export PATH=$DART_SDK/bin:/root/.pub-cache/bin:$PATH
+
+ SDK="dartsdk-${{ matrix.target }}-release.zip"
+ unzip "$SDK" && mv dart-sdk "$DART_SDK" && rm "$SDK"
+
+ dart pub get
+ dart run grinder pkg-standalone-${{ matrix.target }}
+ EOF
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-${{ matrix.target }}
+ path: build/*.tar.gz
+ if-no-files-found: error
+ compression-level: 0
diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml
new file mode 100644
index 000000000..660ceadfa
--- /dev/null
+++ b/.github/workflows/build-macos.yml
@@ -0,0 +1,37 @@
+name: Build for macos
+
+on:
+ workflow_call:
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: Build
+
+ runs-on: ${{ matrix.runner }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - arch: x64
+ runner: macos-13
+ - arch: arm64
+ runner: macos-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Build
+ run: dart run grinder pkg-standalone-macos-${{ matrix.arch }}
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-macos-${{ matrix.arch }}
+ path: build/*.tar.gz
+ if-no-files-found: error
+ compression-level: 0
diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
new file mode 100644
index 000000000..86ea17c22
--- /dev/null
+++ b/.github/workflows/build-windows.yml
@@ -0,0 +1,39 @@
+name: Build for windows
+
+on:
+ workflow_call:
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: Build
+
+ runs-on: ${{ matrix.runner }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - arch: x64
+ runner: windows-latest
+ - arch: ia32
+ runner: windows-latest
+ - arch: arm64
+ runner: windows-arm64
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Build
+ run: dart run grinder pkg-standalone-windows-${{ matrix.arch }}
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-windows-${{ matrix.arch }}
+ path: build/*.zip
+ if-no-files-found: error
+ compression-level: 0
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 16c4ec7ec..cabc84e11 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,14 +1,5 @@
name: CI
-defaults:
- run: {shell: bash}
-
-# The default Node version lives in ../util/initialize/action.yml. It should be
-# kept up-to-date with the latest Node LTS releases, along with the various
-# node-version matrices below.
-#
-# Next update: April 2021
-
on:
push:
branches: [main, feature.*]
@@ -16,634 +7,34 @@ on:
pull_request:
jobs:
- format:
- name: Code formatting
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart format --fix .
- - run: git diff --exit-code
-
- static_analysis:
- name: Static analysis
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: Analyze Dart
- run: dart analyze --fatal-warnings ./
-
- dartdoc:
- name: Dartdoc
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: dartdoc sass
- run: dart run dartdoc --quiet --no-generate-docs
- --errors ambiguous-doc-reference,broken-link,deprecated
- --errors unknown-directive,unknown-macro,unresolved-doc-reference
- - name: dartdoc sass_api
- run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs
- --errors ambiguous-doc-reference,broken-link,deprecated
- --errors unknown-directive,unknown-macro,unresolved-doc-reference
-
- sass_spec_language:
- name: "Language Tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.async_label }}"
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- dart_channel: [stable, dev]
- async_label: [synchronous]
- async_args: ['']
- include:
- - dart_channel: stable
- async_label: asynchronous
- async_args: '--cmd-args --async'
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with:
- dart-sdk: ${{ matrix.dart_channel }}
- github-token: ${{ github.token }}
- - uses: ./.github/util/sass-spec
-
- - name: Run specs
- run: npm run sass-spec -- --dart .. $extra_args
- working-directory: sass-spec
- env: {extra_args: "${{ matrix.async_args }}"}
-
- # The versions should be kept up-to-date with the latest LTS Node releases.
- # They next need to be rotated April 2021. See
- # https://github.com/nodejs/Release.
- sass_spec_js:
- name: "JS API Tests | Pure JS | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}"
- runs-on: "${{ matrix.os }}"
-
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
- dart_channel: [stable]
- node-version: [18]
- include:
- # Include LTS versions on Ubuntu
- - os: ubuntu-latest
- dart_channel: stable
- node-version: 16
- - os: ubuntu-latest
- dart_channel: stable
- node-version: 14
- - os: ubuntu-latest
- dart_channel: dev
- node-version: 18
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with:
- dart-sdk: ${{ matrix.dart_channel }}
- github-token: ${{ github.token }}
- node-version: ${{ matrix.node-version }}
- - uses: ./.github/util/sass-spec
-
- - name: Build JS
- run: dart run grinder pkg-npm-dev
-
- - name: Check out Sass specification
- uses: sass/clone-linked-repo@v1
- with:
- repo: sass/sass
- path: language
-
- - name: Run tests
- run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm
- working-directory: sass-spec
-
- # The versions should be kept up-to-date with the latest LTS Node releases.
- # They next need to be rotated October 2021. See
- # https://github.com/nodejs/Release.
- sass_spec_js_embedded:
- name: 'JS API Tests | Embedded | Node ${{ matrix.node-version }} | ${{ matrix.os }}'
- runs-on: ${{ matrix.os }}-latest
- if: "github.event_name != 'pull_request' || !contains(github.event.pull_request.body, 'skip sass-embedded')"
-
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu, windows, macos]
- node-version: [18]
- include:
- # Include LTS versions on Ubuntu
- - os: ubuntu
- node-version: 16
- - os: ubuntu
- node-version: 14
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with:
- github-token: ${{ github.token }}
- node-version: ${{ matrix.node-version }}
- - uses: ./.github/util/sass-spec
-
- - name: Check out the embedded host
- uses: sass/clone-linked-repo@v1
- with: {repo: sass/embedded-host-node}
-
- - name: Check out the language repo
- uses: sass/clone-linked-repo@v1
- with: {repo: sass/sass, path: build/language}
-
- - name: Initialize embedded host
- run: |
- npm install
- npm run init -- --compiler-path=.. --language-path=../build/language
- npm run compile
- mv {`pwd`/,dist/}lib/src/vendor/dart-sass
- working-directory: embedded-host-node
-
- - name: Version info
- run: |
- path=embedded-host-node/dist/lib/src/vendor/dart-sass/sass
- if [[ -f "$path.cmd" ]]; then "./$path.cmd" --version
- elif [[ -f "$path.bat" ]]; then "./$path.bat" --version
- elif [[ -f "$path.exe" ]]; then "./$path.exe" --version
- else "./$path" --version
- fi
-
- - name: Run tests
- run: npm run js-api-spec -- --sassPackage ../embedded-host-node --sassSassRepo ../build/language
- working-directory: sass-spec
-
- sass_spec_js_browser:
- name: "JS API Tests | Browser | Dart ${{ matrix.dart_channel }}"
-
- strategy:
- matrix:
- dart_channel: [stable]
- fail-fast: false
-
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - uses: browser-actions/setup-chrome@v1
- - uses: ./.github/util/initialize
- with:
- dart-sdk: ${{ matrix.dart_channel }}
- github-token: ${{ github.token }}
- - uses: ./.github/util/sass-spec
-
- - name: Build JS
- run: dart run grinder pkg-npm-dev
-
- - name: Install built dependencies
- run: npm install
- working-directory: build/npm
-
- - name: Check out Sass specification
- uses: sass/clone-linked-repo@v1
- with:
- repo: sass/sass
- path: language
-
- - name: Run tests
- run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm --browser
- working-directory: sass-spec
- env:
- CHROME_EXECUTABLE: chrome
-
- dart_tests:
- name: "Dart tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.os }}"
- runs-on: "${{ matrix.os }}"
-
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
- dart_channel: [stable]
- # TODO(nweiz): Re-enable this when
- # https://github.com/dart-lang/sdk/issues/52121#issuecomment-1728534228
- # is addressed.
- # include: [{os: ubuntu-latest, dart_channel: dev}]
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with:
- dart-sdk: ${{ matrix.dart_channel }}
- github-token: ${{ github.token }}
-
- - run: dart run grinder pkg-standalone-dev
- - name: Run tests
- run: dart run test -x node
-
- # Unit tests that use Node.js, defined in test/.
- #
- # The versions should be kept up-to-date with the latest LTS Node releases.
- # They next need to be rotated April 2021. See
- # https://github.com/nodejs/Release.
- node_tests:
- name: "Node tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}"
- runs-on: "${{ matrix.os }}"
-
- strategy:
- fail-fast: false
-
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
- dart_channel: [stable]
- node-version: [18]
- include:
- # Include LTS versions on Ubuntu
- - os: ubuntu-latest
- dart_channel: stable
- node-version: 16
- - os: ubuntu-latest
- dart_channel: stable
- node-version: 14
- - os: ubuntu-latest
- dart_channel: dev
- node-version: 18
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with:
- dart-sdk: ${{ matrix.dart_channel }}
- github-token: ${{ github.token }}
- node-version: ${{ matrix.node-version }}
-
- - run: dart run grinder pkg-npm-dev
- - name: Run tests
- run: dart run test -t node -j 2
-
- browser_tests:
- name: "Browser Tests | Dart ${{ matrix.dart_channel }}"
-
- strategy:
- matrix:
- dart_channel: [stable]
- fail-fast: false
-
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - uses: browser-actions/setup-chrome@v1
- - uses: ./.github/util/initialize
- with:
- dart-sdk: ${{ matrix.dart_channel }}
- github-token: ${{ github.token }}
-
- - run: dart run grinder pkg-npm-dev
- - name: Run tests
- run: dart run test -p chrome -j 2
- env:
- CHROME_EXECUTABLE: chrome
+ test:
+ uses: ./.github/workflows/test.yml
+ secrets: inherit
double_check:
name: Double-check
runs-on: ubuntu-latest
- needs:
- - sass_spec_language
- - sass_spec_js
- - sass_spec_js_browser
- - sass_spec_js_embedded
- - dart_tests
- - node_tests
- - browser_tests
- - static_analysis
- - dartdoc
- - format
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
+ needs: [test]
+ if: "startsWith(github.ref, 'refs/tags/') && github.event.repository.fork == false"
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: ./.github/util/initialize
with: {github-token: "${{ github.token }}"}
- name: Run checks
run: dart run grinder double-check-before-release
- bootstrap:
- name: "Bootstrap ${{ matrix.bootstrap_version }}"
- runs-on: ubuntu-latest
- needs: [double_check]
-
- strategy:
- fail-fast: false
- matrix:
- bootstrap_version: [4, 5]
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - run: dart run grinder fetch-bootstrap${{matrix.bootstrap_version}}
- env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
- - name: Build
- run: dart bin/sass.dart --quiet build/bootstrap/scss:build/bootstrap-output
-
- bourbon:
- name: Bourbon
- runs-on: ubuntu-latest
- needs: [double_check]
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - run: dart run grinder fetch-bourbon
- env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
- - name: Test
- run: |
- dart bin/sass.dart --quiet -I build/bourbon -I build/bourbon/spec/fixtures \
- build/bourbon/spec/fixtures:build/bourbon-output
-
- foundation:
- name: Foundation
- runs-on: ubuntu-latest
- needs: [double_check]
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - run: dart run grinder fetch-foundation
- env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
- # TODO(nweiz): Foundation has proper Sass tests, but they're currently not
- # compatible with Dart Sass. Once they are, we should run those rather
- # than just building the CSS output.
- - name: Build
- run: dart bin/sass.dart --quiet build/foundation-sites/assets:build/foundation-output
-
- bulma:
- name: Bulma
- runs-on: ubuntu-latest
+ test_vendor:
needs: [double_check]
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - run: dart run grinder fetch-bulma
- env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
- - name: Build
- run: dart bin/sass.dart --quiet build/bulma/bulma.sass build/bulma-output.css
-
- deploy_github_linux:
- name: "Deploy Github: linux-ia32, linux-x64"
- runs-on: ubuntu-latest
- needs: [bootstrap, bourbon, foundation, bulma]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: Deploy
- run: dart run grinder pkg-github-release pkg-github-linux-ia32 pkg-github-linux-x64
- env:
- GH_TOKEN: "${{ secrets.GH_TOKEN }}"
- GH_USER: sassbot
-
- deploy_github_linux_qemu:
- name: "Deploy Github: linux-${{ matrix.arch }}"
- runs-on: ubuntu-latest
- strategy:
- matrix:
- include:
- - arch: arm
- platform: linux/arm/v7
- - arch: arm64
- platform: linux/arm64
- needs: [deploy_github_linux]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - uses: docker/setup-qemu-action@v2
- - name: Deploy
- run: |
- docker run --rm \
- --env "GH_TOKEN=$GH_TOKEN" \
- --env "GH_USER=$GH_USER" \
- --platform ${{ matrix.platform }} \
- --volume "$PWD:$PWD" \
- --workdir "$PWD" \
- docker.io/library/dart:latest \
- /bin/sh -c "dart pub get && dart run grinder pkg-github-linux-${{ matrix.arch }}"
- env:
- GH_TOKEN: "${{ secrets.GH_TOKEN }}"
- GH_USER: sassbot
-
- deploy_github:
- name: "Deploy Github: ${{ matrix.platform }}"
- runs-on: ${{ matrix.runner }}
- needs: [deploy_github_linux]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
- strategy:
- matrix:
- include:
- - runner: macos-latest
- platform: macos-x64
- architecture: x64
- - runner: self-hosted
- platform: macos-arm64
- architecture: arm64
- - runner: windows-latest
- platform: windows
- architecture: x64
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- # Workaround for dart-lang/setup-dart#59
- with:
- github-token: ${{ github.token }}
- architecture: ${{ matrix.architecture }}
-
- - name: Deploy
- run: dart run grinder pkg-github-${{ matrix.platform }}
- env:
- GH_TOKEN: "${{ secrets.GH_TOKEN }}"
- GH_USER: sassbot
-
- deploy_npm:
- name: Deploy npm
- runs-on: ubuntu-latest
- needs: [bootstrap, bourbon, foundation, bulma]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: Deploy
- run: dart run grinder pkg-npm-deploy
- env:
- NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
-
- deploy_bazel:
- name: Deploy Bazel
- runs-on: ubuntu-latest
- needs: [deploy_npm]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: Deploy
- run: dart run grinder update-bazel
- env:
- GH_TOKEN: "${{ secrets.GH_TOKEN }}"
- GH_USER: sassbot
-
- deploy_pub:
- name: "Deploy Pub"
- runs-on: ubuntu-latest
- needs: [bootstrap, bourbon, foundation, bulma]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: Deploy
- run: dart run grinder protobuf pkg-pub-deploy
- env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"}
-
- deploy_sub_packages:
- name: "Deploy Sub-Packages"
- runs-on: ubuntu-latest
- needs: [deploy_pub]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: Deploy
- run: dart run grinder deploy-sub-packages
- env:
- PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"
- GH_TOKEN: "${{ secrets.GH_TOKEN }}"
- GH_USER: sassbot
-
- deploy_homebrew:
- name: "Deploy Homebrew"
- runs-on: ubuntu-latest
- needs: [bootstrap, bourbon, foundation, bulma]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: dart-lang/setup-dart@v1
- - run: dart pub get
-
- - name: Deploy
- run: dart run grinder pkg-homebrew-update
- env:
- GH_TOKEN: "${{ secrets.GH_TOKEN }}"
- GH_USER: sassbot
-
- deploy_chocolatey:
- name: "Deploy Chocolatey"
- runs-on: windows-latest
- needs: [bootstrap, bourbon, foundation, bulma]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
-
- steps:
- - uses: actions/checkout@v3
- - uses: ./.github/util/initialize
- with: {github-token: "${{ github.token }}"}
-
- - name: Deploy
- run: dart run grinder pkg-chocolatey-deploy
- env: {CHOCOLATEY_TOKEN: "${{ secrets.CHOCOLATEY_TOKEN }}"}
-
- deploy_website:
- name: "Deploy sass-lang.com"
- runs-on: ubuntu-latest
- needs: [bootstrap, bourbon, foundation, bulma]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
- steps:
- - uses: actions/checkout@v3
- with:
- repository: sass/sass-site
- token: ${{ secrets.SASS_SITE_TOKEN }}
-
- - uses: EndBug/add-and-commit@v9
- with:
- author_name: Sass Bot
- author_email: sass.bot.beep.boop@gmail.com
- message: Cut a release for a new Dart Sass version
- commit: --allow-empty
-
- release_embedded_host:
- name: "Release Embedded Host"
- runs-on: ubuntu-latest
- needs: [deploy_github_linux, deploy_github_linux_qemu, deploy_github]
- if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'"
- steps:
- - uses: actions/checkout@v3
- with:
- repository: sass/embedded-host-node
- token: ${{ secrets.GH_TOKEN }}
-
- - name: Get version
- id: version
- run: echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT"
-
- - name: Update version
- run: |
- # Update binary package versions
- for dir in $(ls npm); do
- cat "npm/$dir/package.json" |
- jq --arg version ${{ steps.version.outputs.version }} '
- .version |= $version
- ' > package.json.tmp &&
- mv package.json.tmp "npm/$dir/package.json"
- done
-
- # Update main package version and dependencies on binary packages
- cat package.json |
- jq --arg version ${{ steps.version.outputs.version }} '
- .version |= $version |
- ."compiler-version" |= $version |
- .optionalDependencies = (.optionalDependencies | .[] |= $version)
- ' > package.json.tmp &&
- mv package.json.tmp package.json
- curl https://raw.githubusercontent.com/sass/dart-sass/${{ steps.version.outputs.version }}/CHANGELOG.md > CHANGELOG.md
- shell: bash
-
- - uses: EndBug/add-and-commit@v9
- with:
- author_name: Sass Bot
- author_email: sass.bot.beep.boop@gmail.com
- message: Update Dart Sass version and release
- tag: ${{ steps.version.outputs.version }}
+ if: "startsWith(github.ref, 'refs/tags/') && github.event.repository.fork == false"
+ uses: ./.github/workflows/test-vendor.yml
+ secrets: inherit
+
+ release:
+ needs: [test_vendor]
+ if: "startsWith(github.ref, 'refs/tags/') && github.event.repository.fork == false"
+ permissions:
+ contents: write
+ uses: ./.github/workflows/release.yml
+ secrets: inherit
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..28fc39218
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,248 @@
+name: Release
+
+on:
+ workflow_call:
+
+jobs:
+ build_linux:
+ uses: ./.github/workflows/build-linux.yml
+ secrets: inherit
+
+ build_macos:
+ uses: ./.github/workflows/build-macos.yml
+ secrets: inherit
+
+ build_windows:
+ uses: ./.github/workflows/build-windows.yml
+ secrets: inherit
+
+ release_github:
+ name: Release Github
+ runs-on: ubuntu-latest
+ needs: [build_linux, build_macos, build_windows]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Deploy
+ run: dart run grinder pkg-github-release
+ env:
+ GH_TOKEN: "${{ secrets.GH_TOKEN }}"
+ GH_USER: sassbot
+
+ deploy_github:
+ name: Deploy Github
+ runs-on: ubuntu-latest
+ needs: [release_github]
+
+ permissions:
+ contents: write
+
+ steps:
+ - name: Download Artifact
+ uses: actions/download-artifact@v4
+
+ - name: Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ build-*/*
+
+ deploy_npm:
+ name: Deploy npm
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Deploy
+ run: dart run grinder pkg-npm-deploy
+ env:
+ UPDATE_SASS_SASS_REPO: false
+ NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
+
+ deploy_bazel:
+ name: Deploy Bazel
+ runs-on: ubuntu-latest
+ needs: [deploy_npm]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Deploy
+ run: dart run grinder update-bazel
+ env:
+ GH_TOKEN: "${{ secrets.GH_TOKEN }}"
+ GH_USER: sassbot
+
+ deploy_pub:
+ name: Deploy Pub
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Deploy
+ run: dart run grinder protobuf pkg-pub-deploy
+ env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"}
+
+ deploy_sass_api:
+ name: Deploy sass_api
+ runs-on: ubuntu-latest
+ needs: [deploy_pub]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Deploy
+ run: dart run grinder deploy-sass-api
+ env:
+ PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"
+ GH_TOKEN: "${{ secrets.GH_TOKEN }}"
+ GH_USER: sassbot
+
+ deploy_sass_parser:
+ name: Deploy sass-parser
+ runs-on: ubuntu-latest
+ needs: [deploy_npm]
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.GH_TOKEN }}
+ # Set up .npmrc file to publish to npm
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 'lts/*'
+ check-latest: true
+ registry-url: 'https://registry.npmjs.org'
+
+ # The repo package has a file dependency, but the released version needs
+ # a real dependency on the released version of Sass.
+ - run: npm install sass@${{ steps.version.outputs.version }}
+ working-directory: pkg/sass-parser/
+
+ - run: npm publish
+ env:
+ NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
+ working-directory: pkg/sass-parser/
+
+ - name: Get version
+ id: version
+ run: |
+ echo "version=$(jq .version pkg/sass-parser/package.json)" | tee --append "$GITHUB_OUTPUT"
+ - run: git tag sass-parser/${{ steps.version.outputs.version }}
+ - run: git push --tag
+
+ deploy_homebrew:
+ name: Deploy Homebrew
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dart-lang/setup-dart@v1
+ - run: dart pub get
+
+ - name: Deploy
+ run: dart run grinder pkg-homebrew-update
+ env:
+ GH_TOKEN: "${{ secrets.GH_TOKEN }}"
+ GH_USER: sassbot
+
+ deploy_chocolatey:
+ name: Deploy Chocolatey
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Deploy
+ run: dart run grinder pkg-chocolatey-deploy
+ env: {CHOCOLATEY_TOKEN: "${{ secrets.CHOCOLATEY_TOKEN }}"}
+
+ deploy_website:
+ name: Deploy sass-lang.com
+ runs-on: ubuntu-latest
+ needs: [deploy_npm]
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ repository: sass/sass-site
+ token: ${{ secrets.SASS_SITE_TOKEN }}
+
+ - name: Get version
+ id: version
+ run: echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT"
+
+ - name: Wait for npm registry's CDN to catch up on replications
+ run: sleep 600
+
+ - name: Update Dart Sass version
+ run: npm install sass@${{ steps.version.outputs.version }}
+
+ - uses: EndBug/add-and-commit@v9
+ with:
+ author_name: Sass Bot
+ author_email: sass.bot.beep.boop@gmail.com
+ message: Cut a release for a new Dart Sass version
+ commit: --allow-empty
+
+ release_embedded_host:
+ name: Release Embedded Host
+ runs-on: ubuntu-latest
+ needs: [deploy_github]
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ repository: sass/embedded-host-node
+ token: ${{ secrets.GH_TOKEN }}
+
+ - name: Get version
+ id: version
+ run: |
+ echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT"
+ echo "protocol_version=$(curl -fsSL -H "Authorization: Bearer ${{ github.token }}" https://raw.githubusercontent.com/sass/sass/HEAD/spec/EMBEDDED_PROTOCOL_VERSION)" | tee --append "$GITHUB_OUTPUT"
+
+ - name: Update version
+ run: |
+ # Update binary package versions
+ for dir in $(ls npm); do
+ cat "npm/$dir/package.json" |
+ jq --arg version ${{ steps.version.outputs.version }} '
+ .version |= $version
+ ' > package.json.tmp &&
+ mv package.json.tmp "npm/$dir/package.json"
+ done
+
+ # Update main package version and dependencies on binary packages
+ cat package.json |
+ jq --arg version ${{ steps.version.outputs.version }} --arg protocol_version ${{ steps.version.outputs.protocol_version }} '
+ .version |= $version |
+ ."compiler-version" |= $version |
+ ."protocol-version" |= $protocol_version |
+ .optionalDependencies = (.optionalDependencies | .[] |= $version)
+ ' > package.json.tmp &&
+ mv package.json.tmp package.json
+ curl -fsSL -H "Authorization: Bearer ${{ github.token }}" https://raw.githubusercontent.com/sass/dart-sass/${{ steps.version.outputs.version }}/CHANGELOG.md > CHANGELOG.md
+ shell: bash
+
+ - uses: EndBug/add-and-commit@v9
+ with:
+ author_name: Sass Bot
+ author_email: sass.bot.beep.boop@gmail.com
+ message: Update Dart Sass version and release
+ tag: ${{ steps.version.outputs.version }}
diff --git a/.github/workflows/test-vendor.yml b/.github/workflows/test-vendor.yml
new file mode 100644
index 000000000..5e71017a1
--- /dev/null
+++ b/.github/workflows/test-vendor.yml
@@ -0,0 +1,72 @@
+name: Test Vendor
+
+on:
+ workflow_call:
+ workflow_dispatch:
+
+jobs:
+ bootstrap:
+ name: "Bootstrap ${{ matrix.bootstrap_version }}"
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ bootstrap_version: [4, 5]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - run: dart run grinder fetch-bootstrap${{matrix.bootstrap_version}}
+ env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
+ - name: Build
+ run: dart bin/sass.dart --quiet build/bootstrap/scss:build/bootstrap-output
+
+ bourbon:
+ name: Bourbon
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - run: dart run grinder fetch-bourbon
+ env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
+ - name: Test
+ run: |
+ dart bin/sass.dart --quiet -I build/bourbon -I build/bourbon/spec/fixtures \
+ build/bourbon/spec/fixtures:build/bourbon-output
+
+ foundation:
+ name: Foundation
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - run: dart run grinder fetch-foundation
+ env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
+ # TODO(nweiz): Foundation has proper Sass tests, but they're currently not
+ # compatible with Dart Sass. Once they are, we should run those rather
+ # than just building the CSS output.
+ - name: Build
+ run: dart bin/sass.dart --quiet build/foundation-sites/assets:build/foundation-output
+
+ bulma:
+ name: Bulma
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - run: dart run grinder fetch-bulma
+ env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"}
+ - name: Build
+ run: dart bin/sass.dart --quiet build/bulma/bulma.scss build/bulma-output.css
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 000000000..4ee23575b
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,391 @@
+name: Test
+
+defaults:
+ run: {shell: bash}
+
+on:
+ workflow_call:
+ workflow_dispatch:
+
+jobs:
+ format:
+ name: Code formatting
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dart-lang/setup-dart@v1
+ - run: dart format --fix .
+ - run: git diff --exit-code
+
+ static_analysis:
+ name: Static analysis
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: Analyze Dart
+ run: dart analyze --fatal-warnings ./
+
+ dartdoc:
+ name: Dartdoc
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - name: dartdoc sass
+ run: dart run dartdoc --quiet --no-generate-docs
+ --errors ambiguous-doc-reference,broken-link,deprecated
+ --errors unknown-directive,unknown-macro,unresolved-doc-reference
+ - name: dartdoc sass_api
+ run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs
+ --errors ambiguous-doc-reference,broken-link,deprecated
+ --errors unknown-directive,unknown-macro,unresolved-doc-reference
+
+ sass_spec_language:
+ name: "Language Tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.async_label }}"
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ dart_channel: [stable, dev]
+ async_label: [synchronous]
+ async_args: ['']
+ include:
+ - dart_channel: stable
+ async_label: asynchronous
+ async_args: '--cmd-args --async'
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ - uses: ./.github/util/sass-spec
+
+ - name: Run specs
+ run: npm run sass-spec -- --dart .. $extra_args
+ working-directory: sass-spec
+ env: {extra_args: "${{ matrix.async_args }}"}
+
+ sass_spec_js:
+ name: "JS API Tests | Pure JS | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}"
+ runs-on: "${{ matrix.os }}"
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ dart_channel: [stable]
+ node-version: ['lts/*']
+ include:
+ # Test older LTS versions
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-1
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-2
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-3
+ # Test LTS version with dart dev channel
+ - os: ubuntu-latest
+ dart_channel: dev
+ node-version: 'lts/*'
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ node-version: ${{ matrix.node-version }}
+ - uses: ./.github/util/sass-spec
+
+ - name: Build JS
+ run: dart run grinder pkg-npm-dev
+ env: {UPDATE_SASS_SASS_REPO: false}
+
+ - name: Check out Sass specification
+ uses: sass/clone-linked-repo@v1
+ with:
+ repo: sass/sass
+ path: language
+
+ - name: Run tests
+ run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm
+ working-directory: sass-spec
+
+ sass_spec_js_embedded:
+ name: 'JS API Tests | Embedded | Node ${{ matrix.node-version }} | ${{ matrix.os }}'
+ runs-on: ${{ matrix.os }}
+ if: "github.event_name != 'pull_request' || !contains(github.event.pull_request.body, 'skip sass-embedded')"
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ node-version: ['lts/*']
+ include:
+ # Test older LTS versions
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-1
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-2
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-3
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with:
+ github-token: ${{ github.token }}
+ node-version: ${{ matrix.node-version }}
+ - uses: ./.github/util/sass-spec
+
+ - name: Check out the embedded host
+ uses: sass/clone-linked-repo@v1
+ with: {repo: sass/embedded-host-node}
+
+ - name: Check out the language repo
+ uses: sass/clone-linked-repo@v1
+ with: {repo: sass/sass, path: build/language}
+
+ - name: Initialize embedded host
+ run: |
+ npm install
+ npm run init -- --compiler-path=.. --language-path=../build/language
+ npm run compile
+ mv {`pwd`/,dist/}lib/src/vendor/dart-sass
+ working-directory: embedded-host-node
+
+ - name: Version info
+ run: |
+ path=embedded-host-node/dist/lib/src/vendor/dart-sass/sass
+ if [[ -f "$path.cmd" ]]; then "./$path.cmd" --version
+ elif [[ -f "$path.bat" ]]; then "./$path.bat" --version
+ elif [[ -f "$path.exe" ]]; then "./$path.exe" --version
+ else "./$path" --version
+ fi
+
+ - name: Run tests
+ run: npm run js-api-spec -- --sassPackage ../embedded-host-node --sassSassRepo ../build/language
+ working-directory: sass-spec
+
+ sass_spec_js_browser:
+ name: "JS API Tests | Browser | Dart ${{ matrix.dart_channel }}"
+
+ strategy:
+ matrix:
+ dart_channel: [stable]
+ fail-fast: false
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: browser-actions/setup-chrome@v1
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ - uses: ./.github/util/sass-spec
+
+ - name: Build JS
+ run: dart run grinder pkg-npm-dev
+ env: {UPDATE_SASS_SASS_REPO: false}
+
+ - name: Install built dependencies
+ run: npm install
+ working-directory: build/npm
+
+ - name: Check out Sass specification
+ uses: sass/clone-linked-repo@v1
+ with:
+ repo: sass/sass
+ path: language
+
+ - name: Run tests
+ run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm --browser
+ working-directory: sass-spec
+ env:
+ CHROME_EXECUTABLE: chrome
+
+ dart_tests:
+ name: "Dart tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.os }}"
+ runs-on: "${{ matrix.os }}"
+
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ dart_channel: [stable]
+ # TODO(nweiz): Re-enable this when
+ # https://github.com/dart-lang/sdk/issues/52121#issuecomment-1728534228
+ # is addressed.
+ # include: [{os: ubuntu-latest, dart_channel: dev}]
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+
+ - run: dart run grinder pkg-standalone-dev
+ - name: Run tests
+ run: dart run test -x node
+
+ # Unit tests that use Node.js, defined in test/.
+ node_tests:
+ name: "Node tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}"
+ runs-on: "${{ matrix.os }}"
+
+ strategy:
+ fail-fast: false
+
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ dart_channel: [stable]
+ node-version: ['lts/*']
+ include:
+ # Test older LTS versions
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-1
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-2
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-3
+ # Test LTS version with dart dev channel
+ - os: ubuntu-latest
+ dart_channel: dev
+ node-version: 'lts/*'
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ node-version: ${{ matrix.node-version }}
+
+ - run: dart run grinder pkg-npm-dev
+ env: {UPDATE_SASS_SASS_REPO: false}
+ - name: Run tests
+ run: dart run test -t node -j 2
+
+ browser_tests:
+ name: "Browser Tests | Dart ${{ matrix.dart_channel }}"
+
+ strategy:
+ matrix:
+ dart_channel: [stable]
+ fail-fast: false
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: browser-actions/setup-chrome@v1
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+
+ - run: dart run grinder pkg-npm-dev
+ env: {UPDATE_SASS_SASS_REPO: false}
+ - run: sudo chmod 4755 /opt/google/chrome/chrome-sandbox
+ - name: Run tests
+ run: dart run test -p chrome -j 2
+ env:
+ CHROME_EXECUTABLE: chrome
+ # See https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md#option-3_the-safest-way
+ CHROME_DEVEL_SANDBOX: /opt/google/chrome/chrome-sandbox
+
+ sass_parser_tests:
+ name: "sass-parser Tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }}"
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ dart_channel: [stable]
+ node-version: ['lts/*']
+ include:
+ # Test older LTS versions
+ #
+ # TODO: Test on lts/-2 and lts/-3 once they support
+ # `structuredClone()` (that is, once they're v18 or later).
+ - os: ubuntu-latest
+ dart_channel: stable
+ node-version: lts/-1
+ # Test LTS version with dart dev channel
+ - os: ubuntu-latest
+ dart_channel: dev
+ node-version: 'lts/*'
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/util/initialize
+ with:
+ dart-sdk: ${{ matrix.dart_channel }}
+ github-token: ${{ github.token }}
+ node-version: ${{ matrix.node-version }}
+
+ - run: dart run grinder pkg-npm-dev
+ env: {UPDATE_SASS_SASS_REPO: false}
+ - run: npm install
+ working-directory: pkg/sass-parser/
+ - name: Run tests
+ run: npm test
+ working-directory: pkg/sass-parser/
+
+ sass_parser_static_analysis:
+ name: "sass-parser Static Analysis"
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with: {node-version: 'lts/*'}
+ - uses: ./.github/util/initialize
+ with: {github-token: "${{ github.token }}"}
+
+ - run: dart run grinder pkg-npm-dev
+ env: {UPDATE_SASS_SASS_REPO: false}
+ - run: npm install
+ working-directory: build/npm/
+ - run: npm install
+ working-directory: pkg/sass-parser/
+ - name: Run static analysis
+ run: npm run check
+ working-directory: pkg/sass-parser/
+
+ # TODO - postcss/postcss#1958: Enable this once PostCSS doesn't have TypeDoc
+ # warnings.
+
+ # sass_parser_typedoc:
+ # name: "sass-parser Typedoc"
+ # runs-on: ubuntu-latest
+ #
+ # steps:
+ # - uses: actions/checkout@v4
+ # - uses: actions/setup-node@v4
+ # with: {node-version: 'lts/*'}
+ # - run: npm install
+ # working-directory: pkg/sass-parser/
+ # - run: npm run typedoc
+ # working-directory: pkg/sass-parser/
diff --git a/.gitignore b/.gitignore
index 2c61888e5..2d01413cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,8 +12,10 @@ pubspec.lock
package-lock.json
/benchmark/source
node_modules/
+dist/
/doc/api
/pkg/*/doc/api
+/pkg/sass-parser/doc
# Generated protocol buffer files.
*.pb*.dart
diff --git a/.pubignore b/.pubignore
index 2fbab300a..08992ce75 100644
--- a/.pubignore
+++ b/.pubignore
@@ -1,5 +1,5 @@
# This should be identical to .gitignore except that it doesn't exclude
-# generated protobuf files.
+# generated Dart files.
.buildlog
.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e56a37ea6..397dc612c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,726 @@
+## 1.81.0
+
+* Fix a few cases where deprecation warnings weren't being emitted for global
+ built-in functions whose names overlap with CSS calculations.
+
+* Add support for the CSS `round()` calculation with a single argument, as long
+ as that argument might be a unitless number.
+
+## 1.80.7
+
+### Embedded Host
+
+* Don't treat `0` as `undefined` for the `green` and `blue` channels in the
+ `LegacyColor` constructor.
+
+## 1.80.6
+
+### Command-Line Interface
+
+* Make `@parcel/watcher` an optional dependency so this can still be installed
+ on operating systems where it's unavailable.
+
+## 1.80.5
+
+### Embedded Host
+
+* Don't produce phantom `@import` deprecations when using an importer with the
+ legacy API.
+
+## 1.80.4
+
+* No user-visible changes.
+
+## 1.80.3
+
+* Fix a bug where `@import url("...")` would crash in plain CSS files.
+
+* Improve consistency of how warnings are emitted by different parts of the
+ compiler. This should result in minimal user-visible changes, but different
+ types of warnings should now respond more reliably to flags like `--quiet`,
+ `--verbose`, and `--silence-deprecation`.
+
+## 1.80.2
+
+* Fix a bug where deprecation warnings were incorrectly emitted for the
+ plain-CSS `invert()` function.
+
+## 1.80.1
+
+* Fix a bug where repeated deprecation warnings were not automatically limited.
+
+## 1.80.0
+
+* `@import` is now officially deprecated, as are global built-in functions that
+ are available within built-in modules. See [the Sass blog post] for more
+ details on the deprecation process.
+
+[the Sass blog post]: https://sass-lang.com/blog/import-is-deprecated/
+
+### Embedded Host
+
+* Fix an error that would sometimes occur when deprecation warnings were
+ emitted when using a custom importer with the legacy API.
+
+## 1.79.6
+
+* Fix a bug where Sass would add an extra `*/` after loud comments with
+ whitespace after an explicit `*/` in the indented syntax.
+
+* **Potentially breaking bug fix:** Adding text after an explicit `*/` in the
+ indented syntax is now an error, rather than silently generating invalid CSS.
+
+### Embedded Host
+
+* Properly export the `SassBoolean` type.
+
+## 1.79.5
+
+* Changes to how `selector.unify()` and `@extend` combine selectors:
+
+ * The relative order of pseudo-classes (like `:hover`) and pseudo-elements
+ (like `::before`) within each original selector is now preserved when
+ they're combined.
+
+ * Pseudo selectors are now consistently placed at the end of the combined
+ selector, regardless of which selector they came from. Previously, this
+ reordering only applied to pseudo-selectors in the second selector.
+
+* Tweak the color transformation matrices for OKLab and OKLCH to match the
+ newer, more accurate values in the CSS spec.
+
+* Fix a slight inaccuracy case when converting to `srgb-linear` and
+ `display-p3`.
+
+* **Potentially breaking bug fix:** `math.unit()` now wraps multiple denominator
+ units in parentheses. For example, `px/(em*em)` instead of `px/em*em`.
+
+### Command-Line Interface
+
+* Use `@parcel/watcher` to watch the filesystem when running from JavaScript and
+ not using `--poll`. This should mitigate more frequent failures users have
+ been seeing since version 4.0.0 of Chokidar, our previous watching tool, was
+ released.
+
+### JS API
+
+* Fix `SassColor.interpolate()` to allow an undefined `options` parameter, as
+ the types indicate.
+
+### Embedded Sass
+
+* Properly pass missing color channel values to and from custom functions.
+
+## 1.79.4
+
+### JS API
+
+* Fix a bug where passing `green` or `blue` to `color.change()` for legacy
+ colors would fail.
+
+## 1.79.3
+
+* Update the `$channel` parameter in the suggested replacement for
+ `color.red()`, `color.green()`, `color.blue()`, `color.hue()`,
+ `color.saturation()`, `color.lightness()`, `color.whiteness()`, and
+ `color.blackness()` to use a quoted string.
+
+## 1.79.2
+
+* Add a `$space` parameter to the suggested replacement for `color.red()`,
+ `color.green()`, `color.blue()`, `color.hue()`, `color.saturation()`,
+ `color.lightness()`, `color.whiteness()`, and `color.blackness()`.
+
+* Update deprecation warnings for the legacy JS API to include a link to
+ [relevant documentation].
+
+[relevant documentation]: https://sass-lang.com/d/legacy-js-api
+
+## 1.79.1
+
+* No user-visible changes.
+
+## 1.79.0
+
+* **Breaking change**: Passing a number with unit `%` to the `$alpha` parameter
+ of `color.change()`, `color.adjust()`, `change-color()`, and `adjust-color()`
+ is now interpreted as a percentage, instead of ignoring the unit. For example,
+ `color.change(red, $alpha: 50%)` now returns `rgb(255 0 0 / 0.5)`.
+
+* **Potentially breaking compatibility fix**: Sass no longer rounds RGB channels
+ to the nearest integer. This means that, for example, `rgb(0 0 1) != rgb(0 0
+ 0.6)`. This matches the latest version of the CSS spec and browser behavior.
+
+* **Potentially breaking compatibility fix**: Passing large positive or negative
+ values to `color.adjust()` can now cause a color's channels to go outside that
+ color's gamut. In most cases this will currently be clipped by the browser and
+ end up showing the same color as before, but once browsers implement gamut
+ mapping it may produce a different result.
+
+* Add support for CSS Color Level 4 [color spaces]. Each color value now tracks
+ its color space along with the values of each channel in that color space.
+ There are two general principles to keep in mind when dealing with new color
+ spaces:
+
+ 1. With the exception of legacy color spaces (`rgb`, `hsl`, and `hwb`), colors
+ will always be emitted in the color space they were defined in unless
+ they're explicitly converted.
+
+ 2. The `color.to-space()` function is the only way to convert a color to
+ another color space. Some built-in functions may do operations in a
+ different color space, but they'll always convert back to the original space
+ afterwards.
+
+* `rgb` colors can now have non-integer channels and channels outside the normal
+ gamut of 0-255. These colors are always emitted using the `rgb()` syntax so
+ that modern browsers that are being displayed on wide-gamut devices can
+ display the most accurate color possible.
+
+* Add support for all the new color syntax defined in Color Level 4, including:
+
+ * `oklab()`, `oklch()`, `lab()`, and `lch()` functions;
+ * a top-level `hwb()` function that matches the space-separated CSS syntax;
+ * and a `color()` function that supports the `srgb`, `srgb-linear`,
+ `display-p3`, `a98-rgb`, `prophoto-rgb`, `rec2020`, `xyz`, `xyz-d50`, and
+ `xyz-d65` color spaces.
+
+* Add new functions for working with color spaces:
+
+ * `color.to-space($color, $space)` converts `$color` to the given `$space`. In
+ most cases this conversion is lossless—the color may end up out-of-gamut for
+ the destination color space, but browsers will generally display it as best
+ they can regardless. However, the `hsl` and `hwb` spaces can't represent
+ out-of-gamut colors and so will be clamped.
+
+ * `color.channel($color, $channel, $space: null)` returns the value of the
+ given `$channel` in `$color`, after converting it to `$space` if necessary.
+ It should be used instead of the old channel-specific functions such as
+ `color.red()` and `color.hue()`.
+
+ * `color.same($color1, $color2)` returns whether two colors represent the same
+ color even across color spaces. It differs from `$color1 == $color2` because
+ `==` never consider colors in different (non-legacy) spaces as equal.
+
+ * `color.is-in-gamut($color, $space: null)` returns whether `$color` is
+ in-gamut for its color space (or `$space` if it's passed).
+
+ * `color.to-gamut($color, $space: null)` returns `$color` constrained to its
+ space's gamut (or to `$space`'s gamut, if passed). This is generally not
+ recommended since even older browsers will display out-of-gamut colors as
+ best they can, but it may be necessary in some cases.
+
+ * `color.space($color)`: Returns the name of `$color`'s color space.
+
+ * `color.is-legacy($color)`: Returns whether `$color` is in a legacy color
+ space (`rgb`, `hsl`, or `hwb`).
+
+ * `color.is-powerless($color, $channel, $space: null)`: Returns whether the
+ given `$channel` of `$color` is powerless in `$space` (or its own color
+ space). A channel is "powerless" if its value doesn't affect the way the
+ color is displayed, such as hue for a color with 0 chroma.
+
+ * `color.is-missing($color, $channel)`: Returns whether `$channel`'s value is
+ missing in `$color`. Missing channels can be explicitly specified using the
+ special value `none` and can appear automatically when `color.to-space()`
+ returns a color with a powerless channel. Missing channels are usually
+ treated as 0, except when interpolating between two colors and in
+ `color.mix()` where they're treated as the same value as the other color.
+
+* Update existing functions to support color spaces:
+
+ * `hsl()` and `color.hwb()` no longer forbid out-of-bounds values. Instead,
+ they follow the CSS spec by clamping them to within the allowed range.
+
+ * `color.change()`, `color.adjust()`, and `color.scale()` now support all
+ channels of all color spaces. However, if you want to modify a channel
+ that's not in `$color`'s own color space, you have to explicitly specify the
+ space with the `$space` parameter. (For backwards-compatibility, this
+ doesn't apply to legacy channels of legacy colors—for example, you can still
+ adjust an `rgb` color's saturation without passing `$space: hsl`).
+
+ * `color.mix()` and `color.invert()` now support the standard CSS algorithm
+ for interpolating between two colors (the same one that's used for gradients
+ and animations). To use this, pass the color space to use for interpolation
+ to the `$method` parameter. For polar color spaces like `hsl` and `oklch`,
+ this parameter also allows you to specify how hue interpolation is handled.
+
+ * `color.complement()` now supports a `$space` parameter that indicates which
+ color space should be used to take the complement.
+
+ * `color.grayscale()` now operates in the `oklch` space for non-legacy colors.
+
+ * `color.ie-hex-str()` now automatically converts its color to the `rgb` space
+ and gamut-maps it so that it can continue to take colors from any color
+ space.
+
+[color spaces]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
+
+* The following functions are now deprecated, and uses should be replaced with
+ the new color-space-aware functions defined above:
+
+ * The `color.red()`, `color.green()`, `color.blue()`, `color.hue()`,
+ `color.saturation()`, `color.lightness()`, `color.whiteness()`, and
+ `color.blackness()` functions, as well as their global counterparts, should
+ be replaced with calls to `color.channel()`.
+
+ * The global `adjust-hue()`, `saturate()`, `desaturate()`, `lighten()`,
+ `darken()`, `transaprentize()`, `fade-out()`, `opacify()`, and `fade-in()`
+ functions should be replaced by `color.adjust()` or `color.scale()`.
+
+* Add a `global-builtin` future deprecation, which can be opted-into with the
+ `--future-deprecation` flag or the `futureDeprecations` option in the JS or
+ Dart API. This emits warnings when any global built-in functions that are
+ now available in `sass:` modules are called. It will become active by default
+ in an upcoming release alongside the `@import` deprecation.
+
+### Dart API
+
+* Added a `ColorSpace` class which represents the various color spaces defined
+ in the CSS spec.
+
+* Added `SassColor.space` which returns a color's color space.
+
+* Added `SassColor.channels` and `.channelsOrNull` which returns a list
+ of channel values, with missing channels converted to 0 or exposed as null,
+ respectively.
+
+* Added `SassColor.isLegacy`, `.isInGamut`, `.channel()`, `.isChannelMissing()`,
+ `.isChannelPowerless()`, `.toSpace()`, `.toGamut()`, `.changeChannels()`, and
+ `.interpolate()` which do the same thing as the Sass functions of the
+ corresponding names.
+
+* `SassColor.rgb()` now allows out-of-bounds and non-integer arguments.
+
+* `SassColor.hsl()` and `.hwb()` now allow out-of-bounds arguments.
+
+* Added `SassColor.hwb()`, `.srgb()`, `.srgbLinear()`, `.displayP3()`,
+ `.a98Rgb()`, `.prophotoRgb()`, `.rec2020()`, `.xyzD50()`, `.xyzD65()`,
+ `.lab()`, `.lch()`, `.oklab()`, `.oklch()`, and `.forSpace()` constructors.
+
+* Deprecated `SassColor.red`, `.green`, `.blue`, `.hue`, `.saturation`,
+ `.lightness`, `.whiteness`, and `.blackness` in favor of
+ `SassColor.channel()`.
+
+* Deprecated `SassColor.changeRgb()`, `.changeHsl()`, and `.changeHwb()` in
+ favor of `SassColor.changeChannels()`.
+
+* Added `SassNumber.convertValueToUnit()` as a shorthand for
+ `SassNumber.convertValue()` with a single numerator.
+
+* Added `InterpolationMethod` and `HueInterpolationMethod` which collectively
+ represent the method to use to interpolate two colors.
+
+### JS API
+
+* While the legacy API has been deprecated since we released the modern API, we
+ now emit warnings when the legacy API is used to make sure users are aware
+ that it will be removed in Dart Sass 2.0.0. In the meantime, you can silence
+ these warnings by passing `legacy-js-api` in `silenceDeprecations` when using
+ the legacy API.
+
+* Modify `SassColor` to accept a new `space` option, with support for all the
+ new color spaces defined in Color Level 4.
+
+* Add `SassColor.space` which returns a color's color space.
+
+* Add `SassColor.channels` and `.channelsOrNull` which returns a list of channel
+ values, with missing channels converted to 0 or exposed as null, respectively.
+
+* Add `SassColor.isLegacy`, `.isInGamut()`, `.channel()`, `.isChannelMissing()`,
+ `.isChannelPowerless()`, `.toSpace()`, `.toGamut()`, `.change()`, and
+ `.interpolate()` which do the same thing as the Sass functions of the
+ corresponding names.
+
+* Deprecate `SassColor.red`, `.green`, `.blue`, `.hue`, `.saturation`,
+ `.lightness`, `.whiteness`, and `.blackness` in favor of
+ `SassColor.channel()`.
+
+### Embedded Sass
+
+* Add `Color` SassScript value, with support for all the new color spaces
+ defined in Color Level 4.
+
+* Remove `RgbColor`, `HslColor` and `HwbColor` SassScript values.
+
+## 1.78.0
+
+* The `meta.feature-exists` function is now deprecated. This deprecation is
+ named `feature-exists`.
+
+* Fix a crash when using `@at-root` without any queries or children in the
+ indented syntax.
+
+### JS API
+
+* Backport the deprecation options (`fatalDeprecations`, `futureDeprecations`,
+ and `silenceDeprecations`) to the legacy JS API. The legacy JS API is itself
+ deprecated, and you should move off of it if possible, but this will allow
+ users of bundlers and other tools that are still using the legacy API to
+ still control deprecation warnings.
+
+* Fix a bug where accessing `SourceSpan.url` would crash when a relative URL was
+ passed to the Sass API.
+
+### Embedded Sass
+
+* Explicitly expose a `sass` executable from the `sass-embedded` npm package.
+ This was intended to be included in 1.63.0, but due to the way
+ platform-specific dependency executables are installed it did not work as
+ intended. Now users can run `npx sass` for local installs or just `sass` when
+ `sass-embedded` is installed globally.
+
+* Add linux-riscv64, linux-musl-riscv64, and android-riscv64 support for the
+ `sass-embedded` npm package.
+
+* Fix an edge case where the Dart VM could hang when shutting down when requests
+ were in flight.
+
+* Fix a race condition where the embedded host could fail to shut down if it was
+ closed around the same time a new compilation was started.
+
+* Fix a bug where parse-time deprecation warnings could not be controlled by
+ the deprecation options in some circumstances.
+
+## 1.77.8
+
+* No user-visible changes.
+
+## 1.77.7
+
+* Declarations that appear after nested rules are deprecated, because the
+ semantics Sass has historically used are different from the semantics
+ specified by CSS. In the future, Sass will adopt the standard CSS semantics.
+
+ See [the Sass website](https://sass-lang.com/d/mixed-decls) for details.
+
+* **Potentially breaking bug fix:** `//` in certain places such as unknown
+ at-rule values was being preserved in the CSS output, leading to potentially
+ invalid CSS. It's now properly parsed as a silent comment and omitted from the
+ CSS output.
+
+## 1.77.6
+
+* Fix a few cases where comments and occasionally even whitespace wasn't allowed
+ between the end of Sass statements and the following semicolon.
+
+## 1.77.5
+
+* Fully trim redundant selectors generated by `@extend`.
+
+## 1.77.4
+
+### Embedded Sass
+
+* Support passing `Version` input for `fatalDeprecations` as string over
+ embedded protocol.
+
+* Fix a bug in the JS Embedded Host where `Version` could be incorrectly accepted
+ as input for `silenceDeprecations` and `futureDeprecations` in pure JS.
+
+## 1.77.3
+
+### Dart API
+
+* `Deprecation.duplicateVariableFlags` has been deprecated and replaced with
+ `Deprecation.duplicateVarFlags` to make it consistent with the
+ `duplicate-var-flags` name used on the command line and in the JS API.
+
+## 1.77.2
+
+* Don't emit deprecation warnings for functions and mixins beginning with `__`.
+
+* Allow user-defined functions whose names begin with `_` and otherwise look
+ like vendor-prefixed functions with special CSS syntax.
+
+### Command-Line Interface
+
+* Properly handle the `--silence-deprecation` flag.
+
+* Handle the `--fatal-deprecation` and `--future-deprecation` flags for
+ `--interactive` mode.
+
+## 1.77.1
+
+* Fix a crash that could come up with importers in certain contexts.
+
+## 1.77.0
+
+* *Don't* throw errors for at-rules in keyframe blocks.
+
+## 1.76.0
+
+* Throw errors for misplaced statements in keyframe blocks.
+
+* Mixins and functions whose names begin with `--` are now deprecated for
+ forwards-compatibility with the in-progress CSS functions and mixins spec.
+ This deprecation is named `css-function-mixin`.
+
+## 1.75.0
+
+* Fix a bug in which stylesheet canonicalization could be cached incorrectly
+ when custom importers or the Node.js package importer made decisions based on
+ the URL of the containing stylesheet.
+
+### JS API
+
+* Allow `importer` to be passed without `url` in `StringOptionsWithImporter`.
+
+## 1.74.1
+
+* No user-visible changes.
+
+## 1.74.0
+
+### JS API
+
+* Add a new top-level `deprecations` object, which contains various
+ `Deprecation` objects that define the different types of deprecation used by
+ the Sass compiler and can be passed to the options below.
+
+* Add a new `fatalDeprecations` compiler option that causes the compiler to
+ error if any deprecation warnings of the provided types are encountered. You
+ can also pass in a `Version` object to treat all deprecations that were active
+ in that Dart Sass version as fatal.
+
+* Add a new `futureDeprecations` compiler option that allows you to opt-in to
+ certain deprecations early (currently just `import`).
+
+* Add a new `silenceDeprecations` compiler option to ignore any deprecation
+ warnings of the provided types.
+
+### Command-Line Interface
+
+* Add a new `--silence-deprecation` flag, which causes the compiler to ignore
+ any deprecation warnings of the provided types.
+
+* Previously, if a future deprecation was passed to `--fatal-deprecation` but
+ not `--future-deprecation`, it would be treated as fatal despite not being
+ enabled. Both flags are now required to treat a future deprecation as fatal
+ with a warning emitted if `--fatal-deprecation` is passed without
+ `--future-deprecation`, matching the JS API's behavior.
+
+### Dart API
+
+* The `compile` methods now take in a `silenceDeprecations` parameter, which
+ causes the compiler to ignore any deprecation warnings of the provided types.
+
+* Add `Deprecation.obsoleteIn` to match the JS API. This is currently null for
+ all deprecations, but will be used once some deprecations become obsolete in
+ Dart Sass 2.0.0.
+
+* **Potentially breaking bug fix:** Fix a bug where `compileStringToResultAsync`
+ ignored `fatalDeprecations` and `futureDeprecations`.
+
+* The behavior around making future deprecations fatal mentioned in the CLI
+ section above has also been changed in the Dart API.
+
+## 1.73.0
+
+* Add support for nesting in plain CSS files. This is not processed by Sass at
+ all; it's emitted exactly as-is in the CSS.
+
+* In certain circumstances, the current working directory was unintentionally
+ being made available as a load path. This is now deprecated. Anyone relying on
+ this should explicitly pass in `.` as a load path or `FilesystemImporter('.')`
+ as the current importer.
+
+* Add linux-riscv64 and windows-arm64 releases.
+
+### Command-Line Interface
+
+* Fix a bug where absolute `file:` URLs weren't loaded for files compiled via
+ the command line unless an unrelated load path was also passed.
+
+* Fix a bug where `--update` would always update files that were specified via
+ absolute path unless an unrelated load path was also passed.
+
+### Dart API
+
+* Add `FilesystemImporter.noLoadPath`, which is a `FilesystemImporter` that can
+ load absolute `file:` URLs and resolve URLs relative to the base file but
+ doesn't load relative URLs from a load path.
+
+* `FilesystemImporter.cwd` is now deprecated. Either use
+ `FilesystemImporter.noLoadPath` if you weren't intending to rely on the load
+ path, or `FilesystemImporter('.')` if you were.
+
+## 1.72.0
+
+* Support adjacent `/`s without whitespace in between when parsing plain CSS
+ expressions.
+
+* Allow the Node.js `pkg:` importer to load Sass stylesheets for `package.json`
+ `exports` field entries without extensions.
+
+* When printing suggestions for variables, use underscores in variable names
+ when the original usage used underscores.
+
+### JavaScript API
+
+* Properly resolve `pkg:` imports with the Node.js package importer when
+ arguments are passed to the JavaScript process.
+
+## 1.71.1
+
+### Command-Line Interface
+
+* Ship the musl Linux release with the proper Dart executable.
+
+### JavaScript API
+
+* Export the `NodePackageImporter` class in ESM mode.
+
+* Allow `NodePackageImporter` to locate a default directory even when the
+ entrypoint is an ESM module.
+
+### Dart API
+
+* Make passing a null argument to `NodePackageImporter()` a static error rather
+ than just a runtime error.
+
+### Embedded Sass
+
+* In the JS Embedded Host, properly install the musl Linux embedded compiler
+ when running on musl Linux.
+
+## 1.71.0
+
+For more information about `pkg:` importers, see [the
+announcement][pkg-importers] on the Sass blog.
+
+[pkg-importers]: https://sass-lang.com/blog/announcing-pkg-importers
+
+### Command-Line Interface
+
+* Add a `--pkg-importer` flag to enable built-in `pkg:` importers. Currently
+ this only supports the Node.js package resolution algorithm, via
+ `--pkg-importer=node`. For example, `@use "pkg:bootstrap"` will load
+ `node_modules/bootstrap/scss/bootstrap.scss`.
+
+### JavaScript API
+
+* Add a `NodePackageImporter` importer that can be passed to the `importers`
+ option. This loads files using the `pkg:` URL scheme according to the Node.js
+ package resolution algorithm. For example, `@use "pkg:bootstrap"` will load
+ `node_modules/bootstrap/scss/bootstrap.scss`. The constructor takes a single
+ optional argument, which indicates the base directory to use when locating
+ `node_modules` directories. It defaults to
+ `path.dirname(require.main.filename)`.
+
+### Dart API
+
+* Add a `NodePackageImporter` importer that can be passed to the `importers`
+ option. This loads files using the `pkg:` URL scheme according to the Node.js
+ package resolution algorithm. For example, `@use "pkg:bootstrap"` will load
+ `node_modules/bootstrap/scss/bootstrap.scss`. The constructor takes a single
+ argument, which indicates the base directory to use when locating
+ `node_modules` directories.
+
+## 1.70.0
+
+### JavaScript API
+
+* Add a `sass.initCompiler()` function that returns a `sass.Compiler` object
+ which supports `compile()` and `compileString()` methods with the same API as
+ the global Sass object. On the Node.js embedded host, each `sass.Compiler`
+ object uses a single long-lived subprocess, making compiling multiple
+ stylesheets much more efficient.
+
+* Add a `sass.initAsyncCompiler()` function that returns a `sass.AsyncCompiler`
+ object which supports `compileAsync()` and `compileStringAsync()` methods with
+ the same API as the global Sass object. On the Node.js embedded host, each
+ `sass.AsynCompiler` object uses a single long-lived subprocess, making
+ compiling multiple stylesheets much more efficient.
+
+### Embedded Sass
+
+* Support the `CompileRequest.silent` field. This allows compilations with no
+ logging to avoid unnecessary request/response cycles.
+
+* The Dart Sass embedded compiler now reports its name as "dart-sass" rather
+ than "Dart Sass", to match the JS API's `info` field.
+
+## 1.69.7
+
+### Embedded Sass
+
+* In the JS Embedded Host, properly install the x64 Dart Sass executable on
+ ARM64 Windows.
+
+## 1.69.6
+
+* Produce better output for numbers with complex units in `meta.inspect()` and
+ debugging messages.
+
+* Escape U+007F DELETE when serializing strings.
+
+* When generating CSS error messages to display in-browser, escape all code
+ points that aren't in the US-ASCII region. Previously only code points U+0100
+ LATIN CAPITAL LETTER A WITH MACRON were escaped.
+
+* Provide official releases for musl LibC and for Android.
+
+* Don't crash when running `meta.apply()` in asynchronous mode.
+
+### JS API
+
+* Fix a bug where certain exceptions could produce `SourceSpan`s that didn't
+ follow the documented `SourceSpan` API.
+
+## 1.69.5
+
+### JS API
+
+* Compatibility with Node.js 21.0.0.
+
+## 1.69.4
+
+* No user-visible changes.
+
+## 1.69.3
+
+### Embedded Sass
+
+* Fix TypeScript type locations in `package.json`.
+
+## 1.69.2
+
+### JS API
+
+* Fix a bug where Sass crashed when running in the browser if there was a global
+ variable named `process`.
+
+## 1.69.1
+
+* No user-visible changes.
+
+## 1.69.0
+
+* Add a `meta.get-mixin()` function that returns a mixin as a first-class Sass
+ value.
+
+* Add a `meta.apply()` mixin that includes a mixin value.
+
+* Add a `meta.module-mixins()` function which returns a map from mixin names in
+ a module to the first-class mixins that belong to those names.
+
+* Add a `meta.accepts-content()` function which returns whether or not a mixin
+ value can take a content block.
+
+* Add support for the relative color syntax from CSS Color 5. This syntax
+ cannot be used to create Sass color values. It is always emitted as-is in the
+ CSS output.
+
+### Dart API
+
+* Deprecate `Deprecation.calcInterp` since it was never actually emitted as a
+ deprecation.
+
+### Embedded Sass
+
+* Fix a rare race condition where the embedded compiler could freeze when a
+ protocol error was immediately followed by another request.
+
## 1.68.0
* Fix the source spans associated with the `abs-percent` deprecation.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 934a0576a..268e118b3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,7 @@ Want to contribute? Great! First, read this page.
* [Before You Contribute](#before-you-contribute)
* [The Small Print](#the-small-print)
+* [Large Language Models](#large-language-models)
* [Development Dependencies](#development-dependencies)
* [Writing Code](#writing-code)
* [Changing the Language](#changing-the-language)
@@ -9,6 +10,7 @@ Want to contribute? Great! First, read this page.
* [Synchronizing](#synchronizing)
* [File Headers](#file-headers)
* [Release Process](#release-process)
+* [Package Structure](#package-structure)
## Before You Contribute
@@ -37,6 +39,17 @@ one above, the
[corporate cla]: https://developers.google.com/open-source/cla/corporate
+## Large Language Models
+
+Do not submit any code or prose written or modified by large language models or
+"artificial intelligence" such as GitHub Copilot or ChatGPT to this project.
+These tools produce code that looks plausible, which means that not only is it
+likely to contain bugs those bugs are likely to be difficult to notice on
+review. In addition, because these models were trained indiscriminately and
+non-consensually on open-source code with a variety of licenses, it's not
+obvious that we have the moral or legal right to redistribute code they
+generate.
+
## Development Dependencies
1. [Install the Dart SDK][]. If you download an archive manually rather than
@@ -233,3 +246,13 @@ few things to do before pushing that tag:
You *don't* need to create tags for packages in `pkg`; that will be handled
automatically by GitHub actions.
+
+## Package Structure
+
+The structure of the Sass package is documented in README.md files in most
+directories under `lib/`. This documentation is intended to help contributors
+quickly build a basic understanding of the structure of the compiler and how its
+various pieces fit together. [`lib/src/README.md`] is a good starting point to get
+an overview of the compiler as a whole.
+
+[`lib/src/README.md`]: lib/src/README.md
diff --git a/README.md b/README.md
index 64bbb851e..de3edaad5 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun**.
-
+
@@ -14,6 +14,8 @@ A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun**.
+
+
@@ -42,6 +44,7 @@ A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun**.
* [Compatibility Policy](#compatibility-policy)
* [Browser Compatibility](#browser-compatibility)
* [Node.js Compatibility](#nodejs-compatibility)
+ * [Invalid CSS](#invalid-css)
* [Embedded Dart Sass](#embedded-dart-sass)
* [Usage](#usage)
* [Behavioral Differences from Ruby Sass](#behavioral-differences-from-ruby-sass)
@@ -200,7 +203,7 @@ files, you'll need to pass a [custom importer] to [`compileString()`] or
[`compile()`]: https://sass-lang.com/documentation/js-api/functions/compile
[`compileAsync()`]: https://sass-lang.com/documentation/js-api/functions/compileAsync
-[custom importer]: https://sass-lang.com/documentation/js-api/interfaces/StringOptionsWithImporter#importer
+[custom importer]: https://sass-lang.com/documentation/js-api/interfaces/stringoptions/#importer
[`compileString()`]: https://sass-lang.com/documentation/js-api/functions/compileString
[`compileStringAsync()`]: https://sass-lang.com/documentation/js-api/functions/compileStringAsync
[legacy API]: #legacy-javascript-api
@@ -403,7 +406,19 @@ releases listed as Current, Active LTS, or Maintenance LTS according to [the
Node.js release page][]. Once a Node.js version is out of LTS, Dart Sass
considers itself free to break support if necessary.
-[the Node.js release page]: https://nodejs.org/en/about/releases/
+[the Node.js release page]: https://nodejs.org/en/about/previous-releases
+
+### Invalid CSS
+
+Changes to the behavior of Sass stylesheets that produce invalid CSS output are
+_not_ considered breaking changes. Such changes are almost always necessary when
+adding support for new CSS features, and delaying all such features until a new
+major version would be unduly burdensome for most users.
+
+For example, when Sass began parsing `calc()` expressions, the invalid
+expression `calc(1 +)` became a Sass error where before it was passed through
+as-is. This was not considered a breaking change, because `calc(1 +)` was never
+valid CSS to begin with.
## Embedded Dart Sass
diff --git a/analysis/README.md b/analysis/README.md
index 87427c39e..6d3352009 100644
--- a/analysis/README.md
+++ b/analysis/README.md
@@ -4,8 +4,7 @@ packages. To use it, add it as a Git dependency to your `pubspec.yaml`:
```yaml
dev_dependencies:
sass_analysis:
- git: {url:
- https://github.com/sass/dart-sass.git, path: analysis}
+ git: {url: https://github.com/sass/dart-sass.git, path: analysis}
```
and include it in your `analysis_options.yaml`:
diff --git a/analysis/pubspec.yaml b/analysis/pubspec.yaml
index 4e9257b82..cbf04dedc 100644
--- a/analysis/pubspec.yaml
+++ b/analysis/pubspec.yaml
@@ -6,7 +6,7 @@ homepage: https://github.com/sass/dart-sass/tree/master/analysis
publish_to: none
environment:
- sdk: ">=2.17.0 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
dependencies:
- lints: ^2.0.0
+ lints: ^4.0.0
diff --git a/bin/sass.dart b/bin/sass.dart
index ad23649d4..78ac31370 100644
--- a/bin/sass.dart
+++ b/bin/sass.dart
@@ -13,8 +13,8 @@ import 'package:sass/src/executable/options.dart';
import 'package:sass/src/executable/repl.dart';
import 'package:sass/src/executable/watch.dart';
import 'package:sass/src/import_cache.dart';
+import 'package:sass/src/importer/filesystem.dart';
import 'package:sass/src/io.dart';
-import 'package:sass/src/logger/deprecation_handling.dart';
import 'package:sass/src/stylesheet_graph.dart';
import 'package:sass/src/utils.dart';
import 'package:sass/src/embedded/executable.dart'
@@ -44,17 +44,14 @@ Future main(List args) async {
return;
}
+ // Eagerly check these so that we fail here and don't hang in watch mode.
+ options.silenceDeprecations;
+ options.futureDeprecations;
+ options.fatalDeprecations;
+
var graph = StylesheetGraph(ImportCache(
- loadPaths: options.loadPaths,
- // This logger is only used for handling fatal/future deprecations
- // during parsing, and is re-used across parses, so we don't want to
- // limit repetition. A separate DeprecationHandlingLogger is created for
- // each compilation, which will limit repetition if verbose is not
- // passed in addition to handling fatal/future deprecations.
- logger: DeprecationHandlingLogger(options.logger,
- fatalDeprecations: options.fatalDeprecations,
- futureDeprecations: options.futureDeprecations,
- limitRepetition: false)));
+ importers: [...options.pkgImporters, FilesystemImporter.noLoadPath],
+ loadPaths: options.loadPaths));
if (options.watch) {
await watch(options, graph);
return;
diff --git a/lib/sass.dart b/lib/sass.dart
index dc94a90fd..19c6f8f87 100644
--- a/lib/sass.dart
+++ b/lib/sass.dart
@@ -31,7 +31,12 @@ export 'src/importer.dart';
export 'src/logger.dart' show Logger;
export 'src/syntax.dart';
export 'src/value.dart'
- hide ColorFormat, SassApiColor, SassApiValue, SpanColorFormat;
+ hide
+ ColorChannel,
+ ColorFormat,
+ LinearChannel,
+ SassApiColorSpace,
+ SpanColorFormat;
export 'src/visitor/serialize.dart' show OutputStyle;
export 'src/evaluation_context.dart' show warn;
@@ -108,13 +113,13 @@ CompileResult compileToResult(String path,
bool verbose = false,
bool sourceMap = false,
bool charset = true,
+ Iterable? silenceDeprecations,
Iterable? fatalDeprecations,
Iterable? futureDeprecations}) =>
c.compile(path,
logger: logger,
importCache: ImportCache(
importers: importers,
- logger: logger ?? Logger.stderr(color: color),
loadPaths: loadPaths,
packageConfig: packageConfig),
functions: functions,
@@ -123,6 +128,7 @@ CompileResult compileToResult(String path,
verbose: verbose,
sourceMap: sourceMap,
charset: charset,
+ silenceDeprecations: silenceDeprecations,
fatalDeprecations: fatalDeprecations,
futureDeprecations: futureDeprecations);
@@ -207,6 +213,7 @@ CompileResult compileStringToResult(String source,
bool verbose = false,
bool sourceMap = false,
bool charset = true,
+ Iterable? silenceDeprecations,
Iterable? fatalDeprecations,
Iterable? futureDeprecations}) =>
c.compileString(source,
@@ -214,7 +221,6 @@ CompileResult compileStringToResult(String source,
logger: logger,
importCache: ImportCache(
importers: importers,
- logger: logger ?? Logger.stderr(color: color),
packageConfig: packageConfig,
loadPaths: loadPaths),
functions: functions,
@@ -225,6 +231,7 @@ CompileResult compileStringToResult(String source,
verbose: verbose,
sourceMap: sourceMap,
charset: charset,
+ silenceDeprecations: silenceDeprecations,
fatalDeprecations: fatalDeprecations,
futureDeprecations: futureDeprecations);
@@ -245,13 +252,13 @@ Future compileToResultAsync(String path,
bool verbose = false,
bool sourceMap = false,
bool charset = true,
+ Iterable? silenceDeprecations,
Iterable? fatalDeprecations,
Iterable? futureDeprecations}) =>
c.compileAsync(path,
logger: logger,
importCache: AsyncImportCache(
importers: importers,
- logger: logger ?? Logger.stderr(color: color),
loadPaths: loadPaths,
packageConfig: packageConfig),
functions: functions,
@@ -260,6 +267,7 @@ Future compileToResultAsync(String path,
verbose: verbose,
sourceMap: sourceMap,
charset: charset,
+ silenceDeprecations: silenceDeprecations,
fatalDeprecations: fatalDeprecations,
futureDeprecations: futureDeprecations);
@@ -285,6 +293,7 @@ Future compileStringToResultAsync(String source,
bool verbose = false,
bool sourceMap = false,
bool charset = true,
+ Iterable? silenceDeprecations,
Iterable? fatalDeprecations,
Iterable? futureDeprecations}) =>
c.compileStringAsync(source,
@@ -292,7 +301,6 @@ Future compileStringToResultAsync(String source,
logger: logger,
importCache: AsyncImportCache(
importers: importers,
- logger: logger ?? Logger.stderr(color: color),
packageConfig: packageConfig,
loadPaths: loadPaths),
functions: functions,
@@ -302,7 +310,10 @@ Future compileStringToResultAsync(String source,
quietDeps: quietDeps,
verbose: verbose,
sourceMap: sourceMap,
- charset: charset);
+ charset: charset,
+ silenceDeprecations: silenceDeprecations,
+ fatalDeprecations: fatalDeprecations,
+ futureDeprecations: futureDeprecations);
/// Like [compileToResult], but returns [CompileResult.css] rather than
/// returning [CompileResult] directly.
diff --git a/lib/src/README.md b/lib/src/README.md
new file mode 100644
index 000000000..e7b53cd91
--- /dev/null
+++ b/lib/src/README.md
@@ -0,0 +1,230 @@
+# The Sass Compiler
+
+* [Life of a Compilation](#life-of-a-compilation)
+ * [Late Parsing](#late-parsing)
+ * [Early Serialization](#early-serialization)
+* [JS Support](#js-support)
+* [APIs](#apis)
+ * [Importers](#importers)
+ * [Custom Functions](#custom-functions)
+ * [Loggers](#loggers)
+* [Built-In Functions](#built-in-functions)
+* [`@extend`](#extend)
+
+This is the root directory of Dart Sass's private implementation libraries. This
+contains essentially all the business logic defining how Sass is actually
+compiled, as well as the APIs that users use to interact with Sass. There are
+two exceptions:
+
+* [`../../bin/sass.dart`] is the entrypoint for the Dart Sass CLI (on all
+ platforms). While most of the logic it runs exists in this directory, it does
+ contain some logic to drive the basic compilation logic and handle errors. All
+ the most complex parts of the CLI, such as option parsing and the `--watch`
+ command, are handled in the [`executable`] directory. Even Embedded Sass runs
+ through this entrypoint, although it gets immediately gets handed off to [the
+ embedded compiler].
+
+ [`../../bin/sass.dart`]: ../../bin/sass.dart
+ [`executable`]: executable
+ [the embedded compiler]: embedded/README.md
+
+* [`../sass.dart`] is the entrypoint for the public Dart API. This is what's
+ loaded when a Dart package imports Sass. It just contains the basic
+ compilation functions, and exports the rest of the public APIs from this
+ directory.
+
+ [`../sass.dart`]: ../sass.dart
+
+Everything else is contained here, and each file and some subdirectories have
+their own documentation. But before you dive into those, let's take a look at
+the general lifecycle of a Sass compilation.
+
+## Life of a Compilation
+
+Whether it's invoked through the Dart API, the JS API, the CLI, or the embedded
+host, the basic process of a Sass compilation is the same. Sass is implemented
+as an AST-walking [interpreter] that operates in roughly three passes:
+
+[interpreter]: https://en.wikipedia.org/wiki/Interpreter_(computing)
+
+1. **Parsing**. The first step of a Sass compilation is always to parse the
+ source file, whether it's SCSS, the indented syntax, or CSS. The parsing
+ logic lives in the [`parse`] directory, while the abstract syntax tree that
+ represents the parsed file lives in [`ast/sass`].
+
+ [`parse`]: parse/README.md
+ [`ast/sass`]: ast/sass/README.md
+
+2. **Evaluation**. Once a Sass file is parsed, it's evaluated by
+ [`visitor/async_evaluate.dart`]. (Why is there both an async and a sync
+ version of this file? See [Synchronizing] for details!) The evaluator handles
+ all the Sass-specific logic: it resolves variables, includes mixins, executes
+ control flow, and so on. As it goes, it builds up a new AST that represents
+ the plain CSS that is the compilation result, which is defined in
+ [`ast/css`].
+
+ [`visitor/async_evaluate.dart`]: visitor/async_evaluate.dart
+ [Synchronizing]: ../../CONTRIBUTING.md#synchronizing
+ [`ast/css`]: ast/css/README.md
+
+ Sass evaluation is almost entirely linear: it begins at the first statement
+ of the file, evaluates it (which may involve evaluating its nested children),
+ adds its result to the CSS AST, and then moves on to the second statement. On
+ it goes until it reaches the end of the file, at which point it's done. The
+ only exception is module resolution: every Sass module has its own compiled
+ CSS AST, and once the entrypoint file is done compiling the evaluator will go
+ back through these modules, resolve `@extend`s across them as necessary, and
+ stitch them together into the final stylesheet.
+
+ SassScript, the expression-level syntax, is handled by the same evaluator.
+ The main difference between SassScript and statement-level evaluation is that
+ the same SassScript values are used during evaluation _and_ as part of the
+ CSS AST. This means that it's possible to end up with a Sass-specific value,
+ such as a map or a first-class function, as the value of a CSS declaration.
+ If that happens, the Serialization phase will signal an error when it
+ encounters the invalid value.
+
+3. **Serialization**. Once we have the CSS AST that represents the compiled
+ stylesheet, we need to convert it into actual CSS text. This is done by
+ [`visitor/serialize.dart`], which walks the AST and builds up a big buffer of
+ the resulting CSS. It uses [a special string buffer] that tracks source and
+ destination locations in order to generate [source maps] as well.
+
+ [`visitor/serialize.dart`]: visitor/serialize.dart
+ [a special string buffer]: util/source_map_buffer.dart
+ [source maps]: https://web.dev/source-maps/
+
+There's actually one slight complication here: the first and second pass aren't
+as separate as they appear. When one Sass stylesheet loads another with `@use`,
+`@forward`, or `@import`, that rule is handled by the evaluator and _only at
+that point_ is the loaded file parsed. So in practice, compilation actually
+switches between parsing and evaluation, although each individual stylesheet
+naturally has to be parsed before it can be evaluated.
+
+### Late Parsing
+
+Some syntax within a stylesheet is only parsed _during_ evaluation. This allows
+authors to use `#{}` interpolation to inject Sass variables and other dynamic
+values into various locations, such as selectors, while still allowing Sass to
+parse them to support features like nesting and `@extend`. The following
+syntaxes are parsed during evaluation:
+
+* [Selectors](parse/selector.dart)
+* [`@keyframes` frames](parse/keyframe_selector.dart)
+* [Media queries](parse/media_query.dart) (for historical reasons, these are
+ parsed before evaluation and then _reparsed_ after they've been fully
+ evaluated)
+
+### Early Serialization
+
+There are also some cases where the evaluator can serialize values before the
+main serialization pass. For example, if you inject a variable into a selector
+using `#{}`, that variable's value has to be converted to a string during
+evaluation so that the evaluator can then parse and handle the newly-generated
+selector. The evaluator does this by invoking the serializer _just_ for that
+specific value. As a rule of thumb, this happens anywhere interpolation is used
+in the original stylesheet, although there are a few other circumstances as
+well.
+
+## JS Support
+
+One of the main benefits of Dart as an implementation language is that it allows
+us to distribute Dart Sass both as an extremely efficient stand-alone executable
+_and_ an easy-to-install pure-JavaScript package, using the dart2js compilation
+tool. However, properly supporting JS isn't seamless. There are two major places
+where we need to think about JS support:
+
+1. When interfacing with the filesystem. None of Dart's IO APIs are natively
+ supported on JS, so for anything that needs to work on both the Dart VM _and_
+ Node.js we define a shim in the [`io`] directory that will be implemented in
+ terms of `dart:io` if we're running on the Dart VM or the `fs` or `process`
+ modules if we're running on Node. (We don't support IO at all on the browser
+ except to print messages to the console.)
+
+ [`io`]: io/README.md
+
+2. When exposing an API. Dart's JS interop is geared towards _consuming_ JS
+ libraries from Dart, not producing a JS library written in Dart, so we have
+ to jump through some hoops to make it work. This is all handled in the [`js`]
+ directory.
+
+ [`js`]: js/README.md
+
+## APIs
+
+One of Sass's core features is its APIs, which not only compile stylesheets but
+also allow users to provide plugins that can be invoked from within Sass. In
+both the JS API, the Dart API, and the embedded compiler, Sass provides three
+types of plugins: importers, custom functions, and loggers.
+
+### Importers
+
+Importers control how Sass loads stylesheets through `@use`, `@forward`, and
+`@import`. Internally, _all_ stylesheet loads are modeled as importers. When a
+user passes a load path to an API or compiles a stylesheet through the CLI, we
+just use the built-in [`FilesystemImporter`] which implements the same interface
+that we make available to users.
+
+[`FilesystemImporter`]: importer/filesystem.dart
+
+In the Dart API, the importer root class is [`importer/async_importer.dart`].
+The JS API and the embedded compiler wrap the Dart importer API in
+[`importer/node_to_dart`] and [`embedded/importer`] respectively.
+
+[`importer/async_importer.dart`]: importer/async_importer.dart
+[`importer/node_to_dart`]: importer/node_to_dart
+[`embedded/importer`]: embedded/importer
+
+### Custom Functions
+
+Custom functions are defined by users of the Sass API but invoked by Sass
+stylesheets. To a Sass stylesheet, they look like any other built-in function:
+users pass SassScript values to them and get SassScript values back. In fact,
+all the core Sass functions are implemented using the Dart custom function API.
+
+Because custom functions take and return SassScript values, that means we need
+to make _all_ values available to the various APIs. For Dart, this is
+straightforward: we need to have objects to represent those values anyway, so we
+just expose those objects publicly (with a few `@internal` annotations here and
+there to hide APIs we don't want users relying on). These value types live in
+the [`value`] directory.
+
+[`value`]: value/README.md
+
+Exposing values is a bit more complex for other platforms. For the JS API, we do
+a bit of metaprogramming in [`js/value`] so that we can return the
+same Dart values we use internally while still having them expose a JS API that
+feels native to that language. For the embedded host, we convert them to and
+from a protocol buffer representation in [`embedded/protofier.dart`].
+
+[`js/value`]: js/value/README.md
+[`embedded/value.dart`]: embedded/value.dart
+
+### Loggers
+
+Loggers are the simplest of the plugins. They're just callbacks that are invoked
+any time Dart Sass would emit a warning (from the language or from `@warn`) or a
+debug message from `@debug`. They're defined in:
+
+* [`logger.dart`](logger.dart) for Dart
+* [`js/logger.dart`](js/logger.dart) for Node
+* [`embedded/logger.dart`](embedded/logger.dart) for the embedded compiler
+
+## Built-In Functions
+
+All of Sass's built-in functions are defined in the [`functions`] directory,
+including both global functions and functions defined in core modules like
+`sass:math`. As mentioned before, these are defined using the standard custom
+function API, although in a few cases they use additional private features like
+the ability to define multiple overloads of the same function name.
+
+[`functions`]: functions/README.md
+
+## `@extend`
+
+The logic for Sass's `@extend` rule is particularly complex, since it requires
+Sass to not only parse selectors but to understand how to combine them and when
+they can be safely optimized away. Most of the logic for this is contained
+within the [`extend`] directory.
+
+[`extend`]: extend/README.md
diff --git a/lib/src/ast/css/README.md b/lib/src/ast/css/README.md
new file mode 100644
index 000000000..e75c3ff5c
--- /dev/null
+++ b/lib/src/ast/css/README.md
@@ -0,0 +1,52 @@
+# CSS Abstract Syntax Tree
+
+This directory contains the abstract syntax tree that represents a plain CSS
+file generated by Sass compilation. It differs from other Sass ASTs in two major
+ways:
+
+1. Instead of being created by [a parser], it's created by [the evaluator] as it
+ traverses the [Sass AST].
+
+ [a parser]: ../../parse/README.md
+ [the evaluator]: ../../visitor/async_evaluate.dart
+ [Sass AST]: ../sass/README.md
+
+2. Because of various Sass features like `@extend` and at-rule hoisting, the CSS
+ AST is mutable even though all other ASTs are immutable.
+
+**Note:** the CSS AST doesn't have its own representation of declaration values.
+Instead, declaration values are represented as [`Value`] objects. This does mean
+that a CSS AST can be in a state where some of its values aren't representable
+in plain CSS (such as maps)—in this case, [the serializer] will emit an error.
+
+[`Value`]: ../../value/README.md
+[the serializer]: ../../visitor/serialize.dart
+
+## Mutable and Immutable Views
+
+Internally, the CSS AST is mutable to allow for operations like hoisting rules
+to the root of the AST and updating existing selectors when `@extend` rules are
+encountered. However, because mutability poses a high risk for "spooky [action
+at a distance]", we limit access to mutating APIs exclusively to the evaluator.
+
+[action at a distance]: https://en.wikipedia.org/wiki/Action_at_a_distance_(computer_programming)
+
+We do this by having an _unmodifiable_ interface (written in this directory) for
+each CSS AST node which only exposes members that don't modify the node in
+question. The implementations of those interfaces, which _do_ have modifying
+methods, live in the [`modifiable`] directory. We then universally refer to the
+immutable node interfaces except specifically in the evaluator, and the type
+system automatically ensures we don't accidentally mutate anything we don't
+intend to.
+
+[`modifiable`]: modifiable
+
+(Of course, it's always possible to cast an immutable node type to a mutable
+one, but that's a very clear code smell that a reviewer can easily identify.)
+
+## CSS Source Files
+
+A lesser-known fact about Sass is that it actually supports _three_ syntaxes for
+its source files: SCSS, the indented syntax, and plain CSS. But even when it
+parses plain CSS, it uses the Sass AST rather than the CSS AST to represent it
+so that parsing logic can easily be shared with the other stylesheet parsers.
diff --git a/lib/src/ast/css/declaration.dart b/lib/src/ast/css/declaration.dart
index 4d5e906cd..2d8785dbf 100644
--- a/lib/src/ast/css/declaration.dart
+++ b/lib/src/ast/css/declaration.dart
@@ -2,10 +2,13 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
+import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
+import 'package:stack_trace/stack_trace.dart';
import '../../value.dart';
import 'node.dart';
+import 'style_rule.dart';
import 'value.dart';
/// A plain CSS declaration (that is, a `name: value` pair).
@@ -16,6 +19,22 @@ abstract interface class CssDeclaration implements CssNode {
/// The value of this declaration.
CssValue get value;
+ /// A list of style rules that appeared before this declaration in the Sass
+ /// input but after it in the CSS output.
+ ///
+ /// These are used to emit mixed declaration deprecation warnings during
+ /// serialization, so we can check based on specificity whether the warnings
+ /// are really necessary without worrying about `@extend` potentially changing
+ /// things up.
+ @internal
+ List get interleavedRules;
+
+ /// The stack trace indicating where this node was created.
+ ///
+ /// This is used to emit interleaved declaration warnings, and is only set if
+ /// [interleavedRules] isn't empty.
+ Trace? get trace;
+
/// The span for [value] that should be emitted to the source map.
///
/// When the declaration's expression is just a variable, this is the span
diff --git a/lib/src/ast/css/media_query.dart b/lib/src/ast/css/media_query.dart
index dc2d5d532..bad97ada6 100644
--- a/lib/src/ast/css/media_query.dart
+++ b/lib/src/ast/css/media_query.dart
@@ -3,7 +3,6 @@
// https://opensource.org/licenses/MIT.
import '../../interpolation_map.dart';
-import '../../logger.dart';
import '../../parse/media_query.dart';
import '../../utils.dart';
@@ -44,9 +43,8 @@ final class CssMediaQuery {
///
/// Throws a [SassFormatException] if parsing fails.
static List parseList(String contents,
- {Object? url, Logger? logger, InterpolationMap? interpolationMap}) =>
- MediaQueryParser(contents,
- url: url, logger: logger, interpolationMap: interpolationMap)
+ {Object? url, InterpolationMap? interpolationMap}) =>
+ MediaQueryParser(contents, url: url, interpolationMap: interpolationMap)
.parse();
/// Creates a media query specifies a type and, optionally, conditions.
diff --git a/lib/src/ast/css/modifiable/declaration.dart b/lib/src/ast/css/modifiable/declaration.dart
index 1acb292a7..3a0279307 100644
--- a/lib/src/ast/css/modifiable/declaration.dart
+++ b/lib/src/ast/css/modifiable/declaration.dart
@@ -3,11 +3,13 @@
// https://opensource.org/licenses/MIT.
import 'package:source_span/source_span.dart';
+import 'package:stack_trace/stack_trace.dart';
import '../../../value.dart';
import '../../../visitor/interface/modifiable_css.dart';
import '../declaration.dart';
import '../value.dart';
+import '../style_rule.dart';
import 'node.dart';
/// A modifiable version of [CssDeclaration] for use in the evaluation step.
@@ -16,6 +18,8 @@ final class ModifiableCssDeclaration extends ModifiableCssNode
final CssValue name;
final CssValue value;
final bool parsedAsCustomProperty;
+ final List interleavedRules;
+ final Trace? trace;
final FileSpan valueSpanForMap;
final FileSpan span;
@@ -23,8 +27,14 @@ final class ModifiableCssDeclaration extends ModifiableCssNode
/// Returns a new CSS declaration with the given properties.
ModifiableCssDeclaration(this.name, this.value, this.span,
- {required this.parsedAsCustomProperty, FileSpan? valueSpanForMap})
- : valueSpanForMap = valueSpanForMap ?? value.span {
+ {required this.parsedAsCustomProperty,
+ Iterable? interleavedRules,
+ this.trace,
+ FileSpan? valueSpanForMap})
+ : interleavedRules = interleavedRules == null
+ ? const []
+ : List.unmodifiable(interleavedRules),
+ valueSpanForMap = valueSpanForMap ?? value.span {
if (parsedAsCustomProperty) {
if (!isCustomProperty) {
throw ArgumentError(
diff --git a/lib/src/ast/css/modifiable/node.dart b/lib/src/ast/css/modifiable/node.dart
index bef0be821..230cab2e7 100644
--- a/lib/src/ast/css/modifiable/node.dart
+++ b/lib/src/ast/css/modifiable/node.dart
@@ -13,11 +13,10 @@ import '../node.dart';
/// modification should only be done within the evaluation step, so the
/// unmodifiable types are used elsewhere to enforce that constraint.
abstract base class ModifiableCssNode extends CssNode {
- /// The node that contains this, or `null` for the root [CssStylesheet] node.
ModifiableCssParentNode? get parent => _parent;
ModifiableCssParentNode? _parent;
- /// The index of [this] in `parent.children`.
+ /// The index of `this` in `parent.children`.
///
/// This makes [remove] more efficient.
int? _indexInParent;
@@ -33,7 +32,7 @@ abstract base class ModifiableCssNode extends CssNode {
T accept(ModifiableCssVisitor visitor);
- /// Removes [this] from [parent]'s child list.
+ /// Removes `this` from [parent]'s child list.
///
/// Throws a [StateError] if [parent] is `null`.
void remove() {
@@ -65,10 +64,10 @@ abstract base class ModifiableCssParentNode extends ModifiableCssNode
: _children = children,
children = UnmodifiableListView(children);
- /// Returns whether [this] is equal to [other], ignoring their child nodes.
+ /// Returns whether `this` is equal to [other], ignoring their child nodes.
bool equalsIgnoringChildren(ModifiableCssNode other);
- /// Returns a copy of [this] with an empty [children] list.
+ /// Returns a copy of `this` with an empty [children] list.
///
/// This is *not* a deep copy. If other parts of this node are modifiable,
/// they are shared between the new and old nodes.
diff --git a/lib/src/ast/css/modifiable/style_rule.dart b/lib/src/ast/css/modifiable/style_rule.dart
index a5d2b1f0c..6e242d36c 100644
--- a/lib/src/ast/css/modifiable/style_rule.dart
+++ b/lib/src/ast/css/modifiable/style_rule.dart
@@ -21,12 +21,13 @@ final class ModifiableCssStyleRule extends ModifiableCssParentNode
final SelectorList originalSelector;
final FileSpan span;
+ final bool fromPlainCss;
/// Creates a new [ModifiableCssStyleRule].
///
/// If [originalSelector] isn't passed, it defaults to [_selector.value].
ModifiableCssStyleRule(this._selector, this.span,
- {SelectorList? originalSelector})
+ {SelectorList? originalSelector, this.fromPlainCss = false})
: originalSelector = originalSelector ?? _selector.value;
T accept(ModifiableCssVisitor visitor) =>
diff --git a/lib/src/ast/css/node.dart b/lib/src/ast/css/node.dart
index 29daba28d..04b6b839d 100644
--- a/lib/src/ast/css/node.dart
+++ b/lib/src/ast/css/node.dart
@@ -15,6 +15,10 @@ import 'style_rule.dart';
/// A statement in a plain CSS syntax tree.
@sealed
abstract class CssNode implements AstNode {
+ /// The node that contains this, or `null` for the root [CssStylesheet] node.
+ @internal
+ CssParentNode? get parent;
+
/// Whether this was generated from the last node in a nested Sass tree that
/// got flattened during evaluation.
bool get isGroupEnd;
diff --git a/lib/src/ast/css/style_rule.dart b/lib/src/ast/css/style_rule.dart
index ccce74fdb..58bbe5424 100644
--- a/lib/src/ast/css/style_rule.dart
+++ b/lib/src/ast/css/style_rule.dart
@@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
+import 'package:meta/meta.dart';
+
import '../selector.dart';
import 'node.dart';
@@ -16,4 +18,10 @@ abstract interface class CssStyleRule implements CssParentNode {
/// The selector for this rule, before any extensions were applied.
SelectorList get originalSelector;
+
+ /// Whether this style rule was originally defined in a plain CSS stylesheet.
+ ///
+ /// @nodoc
+ @internal
+ bool get fromPlainCss;
}
diff --git a/lib/src/ast/css/stylesheet.dart b/lib/src/ast/css/stylesheet.dart
index 08a671cde..45d4d97bb 100644
--- a/lib/src/ast/css/stylesheet.dart
+++ b/lib/src/ast/css/stylesheet.dart
@@ -13,6 +13,7 @@ import 'node.dart';
///
/// This is the root plain CSS node. It contains top-level statements.
class CssStylesheet extends CssParentNode {
+ CssParentNode? get parent => null;
final List children;
final FileSpan span;
bool get isGroupEnd => false;
diff --git a/lib/src/ast/sass/README.md b/lib/src/ast/sass/README.md
new file mode 100644
index 000000000..415a4b6ae
--- /dev/null
+++ b/lib/src/ast/sass/README.md
@@ -0,0 +1,34 @@
+# Sass Abstract Syntax Tree
+
+This directory contains the abstract syntax tree that represents a Sass source
+file, regardless of which syntax it was written in (SCSS, the indented syntax,
+or plain CSS). The AST is constructed recursively by [a parser] from the leaf
+nodes in towards the root, which allows it to be fully immutable.
+
+[a parser]: ../../parse/README.md
+
+The Sass AST is broken up into three categories:
+
+1. The [statement AST], which represents statement-level constructs like
+ variable assignments, style rules, and at-rules.
+
+ [statement AST]: statement
+
+2. The [expression AST], which represents SassScript expressions like function
+ calls, operations, and value literals.
+
+ [expression AST]: exprssion
+
+3. Miscellaneous AST nodes that are used by both statements and expressions or
+ don't fit cleanly into either category that live directly in this directory.
+
+The Sass AST nodes are processed (usually from the root [`Stylesheet`]) by [the
+evaluator], which runs the logic they encode and builds up a [CSS AST] that
+represents the compiled stylesheet. They can also be transformed back into Sass
+source using the `toString()` method. Since this is only ever used for debugging
+and doesn't need configuration or full-featured indentation tracking, it doesn't
+use a full visitor.
+
+[`Stylesheet`]: statement/stylesheet.dart
+[the evaluator]: ../../visitor/async_evaluate.dart
+[CSS AST]: ../css/README.md
diff --git a/lib/src/ast/sass/argument_declaration.dart b/lib/src/ast/sass/argument_declaration.dart
index 7ab73c3a5..ed1951cad 100644
--- a/lib/src/ast/sass/argument_declaration.dart
+++ b/lib/src/ast/sass/argument_declaration.dart
@@ -5,7 +5,6 @@
import 'package:source_span/source_span.dart';
import '../../exception.dart';
-import '../../logger.dart';
import '../../parse/scss.dart';
import '../../util/character.dart';
import '../../util/span.dart';
@@ -71,9 +70,8 @@ final class ArgumentDeclaration implements SassNode {
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
- factory ArgumentDeclaration.parse(String contents,
- {Object? url, Logger? logger}) =>
- ScssParser(contents, url: url, logger: logger).parseArgumentDeclaration();
+ factory ArgumentDeclaration.parse(String contents, {Object? url}) =>
+ ScssParser(contents, url: url).parseArgumentDeclaration();
/// Throws a [SassScriptException] if [positional] and [names] aren't valid
/// for this argument declaration.
diff --git a/lib/src/ast/sass/at_root_query.dart b/lib/src/ast/sass/at_root_query.dart
index 3bad9cf20..0ed522c39 100644
--- a/lib/src/ast/sass/at_root_query.dart
+++ b/lib/src/ast/sass/at_root_query.dart
@@ -7,7 +7,6 @@ import 'package:collection/collection.dart';
import '../../exception.dart';
import '../../interpolation_map.dart';
-import '../../logger.dart';
import '../../parse/at_root_query.dart';
import '../css.dart';
@@ -58,10 +57,10 @@ final class AtRootQuery {
///
/// Throws a [SassFormatException] if parsing fails.
factory AtRootQuery.parse(String contents,
- {Object? url, Logger? logger, InterpolationMap? interpolationMap}) =>
- AtRootQueryParser(contents, url: url, logger: logger).parse();
+ {Object? url, InterpolationMap? interpolationMap}) =>
+ AtRootQueryParser(contents, url: url).parse();
- /// Returns whether [this] excludes [node].
+ /// Returns whether `this` excludes [node].
///
/// @nodoc
@internal
@@ -76,6 +75,6 @@ final class AtRootQuery {
};
}
- /// Returns whether [this] excludes an at-rule with the given [name].
+ /// Returns whether `this` excludes an at-rule with the given [name].
bool excludesName(String name) => (_all || names.contains(name)) != include;
}
diff --git a/lib/src/ast/sass/expression.dart b/lib/src/ast/sass/expression.dart
index 051e1c269..74a4888db 100644
--- a/lib/src/ast/sass/expression.dart
+++ b/lib/src/ast/sass/expression.dart
@@ -2,113 +2,50 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
-import 'package:charcode/charcode.dart';
import 'package:meta/meta.dart';
import '../../exception.dart';
-import '../../logger.dart';
import '../../parse/scss.dart';
-import '../../util/nullable.dart';
-import '../../value.dart';
import '../../visitor/interface/expression.dart';
+import '../../visitor/is_calculation_safe.dart';
+import '../../visitor/source_interpolation.dart';
import '../sass.dart';
+// Note: this has to be a concrete class so we can expose its accept() function
+// to the JS parser.
+
/// A SassScript expression in a Sass syntax tree.
///
/// {@category AST}
/// {@category Parsing}
@sealed
-abstract interface class Expression implements SassNode {
+abstract class Expression implements SassNode {
/// Calls the appropriate visit method on [visitor].
T accept(ExpressionVisitor visitor);
- /// Parses an expression from [contents].
- ///
- /// If passed, [url] is the name of the file from which [contents] comes.
- ///
- /// Throws a [SassFormatException] if parsing fails.
- factory Expression.parse(String contents, {Object? url, Logger? logger}) =>
- ScssParser(contents, url: url, logger: logger).parseExpression();
-}
+ Expression();
-// Use an extension class rather than a method so we don't have to make
-// [Expression] a concrete base class for something we'll get rid of anyway once
-// we remove the global math functions that make this necessary.
-extension ExpressionExtensions on Expression {
/// Whether this expression can be used in a calculation context.
+ bool get isCalculationSafe => accept(const IsCalculationSafeVisitor());
+
+ /// If this expression is valid interpolated plain CSS, returns the equivalent
+ /// of parsing its source as an interpolated unknown value.
+ ///
+ /// Otherwise, returns null.
///
/// @nodoc
@internal
- bool get isCalculationSafe => accept(_IsCalculationSafeVisitor());
-}
-
-// We could use [AstSearchVisitor] to implement this more tersely, but that
-// would default to returning `true` if we added a new expression type and
-// forgot to update this class.
-class _IsCalculationSafeVisitor implements ExpressionVisitor {
- const _IsCalculationSafeVisitor();
-
- bool visitBinaryOperationExpression(BinaryOperationExpression node) =>
- (const {
- BinaryOperator.times,
- BinaryOperator.dividedBy,
- BinaryOperator.plus,
- BinaryOperator.minus
- }).contains(node.operator) &&
- (node.left.accept(this) || node.right.accept(this));
-
- bool visitBooleanExpression(BooleanExpression node) => false;
-
- bool visitColorExpression(ColorExpression node) => false;
-
- bool visitFunctionExpression(FunctionExpression node) => true;
-
- bool visitInterpolatedFunctionExpression(
- InterpolatedFunctionExpression node) =>
- true;
-
- bool visitIfExpression(IfExpression node) => true;
-
- bool visitListExpression(ListExpression node) =>
- node.separator == ListSeparator.space &&
- !node.hasBrackets &&
- node.contents.length > 1 &&
- node.contents.every((expression) => expression.accept(this));
-
- bool visitMapExpression(MapExpression node) => false;
-
- bool visitNullExpression(NullExpression node) => false;
-
- bool visitNumberExpression(NumberExpression node) => true;
-
- bool visitParenthesizedExpression(ParenthesizedExpression node) =>
- node.expression.accept(this);
-
- bool visitSelectorExpression(SelectorExpression node) => false;
-
- bool visitStringExpression(StringExpression node) {
- if (node.hasQuotes) return false;
-
- // Exclude non-identifier constructs that are parsed as [StringExpression]s.
- // We could just check if they parse as valid identifiers, but this is
- // cheaper.
- var text = node.text.initialPlain;
- return
- // !important
- !text.startsWith("!") &&
- // ID-style identifiers
- !text.startsWith("#") &&
- // Unicode ranges
- text.codeUnitAtOrNull(1) != $plus &&
- // url()
- text.codeUnitAtOrNull(3) != $lparen;
+ Interpolation? get sourceInterpolation {
+ var visitor = SourceInterpolationVisitor();
+ accept(visitor);
+ return visitor.buffer?.interpolation(span);
}
- bool visitSupportsExpression(SupportsExpression node) => false;
-
- bool visitUnaryOperationExpression(UnaryOperationExpression node) => false;
-
- bool visitValueExpression(ValueExpression node) => false;
-
- bool visitVariableExpression(VariableExpression node) => true;
+ /// Parses an expression from [contents].
+ ///
+ /// If passed, [url] is the name of the file from which [contents] comes.
+ ///
+ /// Throws a [SassFormatException] if parsing fails.
+ factory Expression.parse(String contents, {Object? url}) =>
+ ScssParser(contents, url: url).parseExpression().$1;
}
diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart
index dc750900a..4e9cda229 100644
--- a/lib/src/ast/sass/expression/binary_operation.dart
+++ b/lib/src/ast/sass/expression/binary_operation.dart
@@ -14,7 +14,7 @@ import 'list.dart';
/// A binary operator, as in `1 + 2` or `$this and $other`.
///
/// {@category AST}
-final class BinaryOperationExpression implements Expression {
+final class BinaryOperationExpression extends Expression {
/// The operator being invoked.
final BinaryOperator operator;
@@ -111,6 +111,9 @@ final class BinaryOperationExpression implements Expression {
///
/// {@category AST}
enum BinaryOperator {
+ // Note: When updating these operators, also update
+ // pkg/sass-parser/lib/src/expression/binary-operation.ts.
+
/// The Microsoft equals operator, `=`.
singleEquals('single equals', '=', 0),
@@ -153,13 +156,13 @@ enum BinaryOperator {
/// The modulo operator, `%`.
modulo('modulo', '%', 6);
- /// The English name of [this].
+ /// The English name of `this`.
final String name;
- /// The Sass syntax for [this].
+ /// The Sass syntax for `this`.
final String operator;
- /// The precedence of [this].
+ /// The precedence of `this`.
///
/// An operator with higher precedence binds tighter.
final int precedence;
diff --git a/lib/src/ast/sass/expression/boolean.dart b/lib/src/ast/sass/expression/boolean.dart
index 23474a3f6..fa79e23a7 100644
--- a/lib/src/ast/sass/expression/boolean.dart
+++ b/lib/src/ast/sass/expression/boolean.dart
@@ -10,7 +10,7 @@ import '../expression.dart';
/// A boolean literal, `true` or `false`.
///
/// {@category AST}
-final class BooleanExpression implements Expression {
+final class BooleanExpression extends Expression {
/// The value of this expression.
final bool value;
diff --git a/lib/src/ast/sass/expression/color.dart b/lib/src/ast/sass/expression/color.dart
index e81a7f8b8..9713b5f75 100644
--- a/lib/src/ast/sass/expression/color.dart
+++ b/lib/src/ast/sass/expression/color.dart
@@ -11,7 +11,7 @@ import '../expression.dart';
/// A color literal.
///
/// {@category AST}
-final class ColorExpression implements Expression {
+final class ColorExpression extends Expression {
/// The value of this color.
final SassColor value;
diff --git a/lib/src/ast/sass/expression/function.dart b/lib/src/ast/sass/expression/function.dart
index 398a2ff03..0f2fce7eb 100644
--- a/lib/src/ast/sass/expression/function.dart
+++ b/lib/src/ast/sass/expression/function.dart
@@ -17,12 +17,18 @@ import '../reference.dart';
/// interpolation.
///
/// {@category AST}
-final class FunctionExpression
- implements Expression, CallableInvocation, SassReference {
+final class FunctionExpression extends Expression
+ implements CallableInvocation, SassReference {
/// The namespace of the function being invoked, or `null` if it's invoked
/// without a namespace.
final String? namespace;
+ /// The name of the function being invoked, with underscores converted to
+ /// hyphens.
+ ///
+ /// If this function is a plain CSS function, use [originalName] instead.
+ final String name;
+
/// The name of the function being invoked, with underscores left as-is.
final String originalName;
@@ -31,12 +37,6 @@ final class FunctionExpression
final FileSpan span;
- /// The name of the function being invoked, with underscores converted to
- /// hyphens.
- ///
- /// If this function is a plain CSS function, use [originalName] instead.
- String get name => originalName.replaceAll('_', '-');
-
FileSpan get nameSpan {
if (namespace == null) return span.initialIdentifier();
return span.withoutNamespace().initialIdentifier();
@@ -46,7 +46,8 @@ final class FunctionExpression
namespace == null ? null : span.initialIdentifier();
FunctionExpression(this.originalName, this.arguments, this.span,
- {this.namespace});
+ {this.namespace})
+ : name = originalName.replaceAll('_', '-');
T accept(ExpressionVisitor visitor) =>
visitor.visitFunctionExpression(this);
diff --git a/lib/src/ast/sass/expression/if.dart b/lib/src/ast/sass/expression/if.dart
index 8805d4bff..95e305e47 100644
--- a/lib/src/ast/sass/expression/if.dart
+++ b/lib/src/ast/sass/expression/if.dart
@@ -14,7 +14,7 @@ import '../../../visitor/interface/expression.dart';
/// evaluated.
///
/// {@category AST}
-final class IfExpression implements Expression, CallableInvocation {
+final class IfExpression extends Expression implements CallableInvocation {
/// The declaration of `if()`, as though it were a normal function.
static final declaration = ArgumentDeclaration.parse(
r"@function if($condition, $if-true, $if-false) {");
diff --git a/lib/src/ast/sass/expression/interpolated_function.dart b/lib/src/ast/sass/expression/interpolated_function.dart
index 3c97b0c9f..cd5e2abf2 100644
--- a/lib/src/ast/sass/expression/interpolated_function.dart
+++ b/lib/src/ast/sass/expression/interpolated_function.dart
@@ -15,8 +15,8 @@ import '../interpolation.dart';
/// This is always a plain CSS function.
///
/// {@category AST}
-final class InterpolatedFunctionExpression
- implements Expression, CallableInvocation {
+final class InterpolatedFunctionExpression extends Expression
+ implements CallableInvocation {
/// The name of the function being invoked.
final Interpolation name;
diff --git a/lib/src/ast/sass/expression/list.dart b/lib/src/ast/sass/expression/list.dart
index 01416afa4..67d26880e 100644
--- a/lib/src/ast/sass/expression/list.dart
+++ b/lib/src/ast/sass/expression/list.dart
@@ -13,7 +13,7 @@ import 'unary_operation.dart';
/// A list literal.
///
/// {@category AST}
-final class ListExpression implements Expression {
+final class ListExpression extends Expression {
/// The elements of this list.
final List contents;
@@ -58,7 +58,7 @@ final class ListExpression implements Expression {
return buffer.toString();
}
- /// Returns whether [expression], contained in [this], needs parentheses when
+ /// Returns whether [expression], contained in `this`, needs parentheses when
/// printed as Sass source.
bool _elementNeedsParens(Expression expression) => switch (expression) {
ListExpression(
diff --git a/lib/src/ast/sass/expression/map.dart b/lib/src/ast/sass/expression/map.dart
index 9bc234780..9bbd540f2 100644
--- a/lib/src/ast/sass/expression/map.dart
+++ b/lib/src/ast/sass/expression/map.dart
@@ -10,7 +10,7 @@ import '../expression.dart';
/// A map literal.
///
/// {@category AST}
-final class MapExpression implements Expression {
+final class MapExpression extends Expression {
/// The pairs in this map.
///
/// This is a list of pairs rather than a map because a map may have two keys
diff --git a/lib/src/ast/sass/expression/null.dart b/lib/src/ast/sass/expression/null.dart
index 4155c00b0..c1f0b583e 100644
--- a/lib/src/ast/sass/expression/null.dart
+++ b/lib/src/ast/sass/expression/null.dart
@@ -10,7 +10,7 @@ import '../expression.dart';
/// A null literal.
///
/// {@category AST}
-final class NullExpression implements Expression {
+final class NullExpression extends Expression {
final FileSpan span;
NullExpression(this.span);
diff --git a/lib/src/ast/sass/expression/number.dart b/lib/src/ast/sass/expression/number.dart
index 7eb2b6fd9..2078e7148 100644
--- a/lib/src/ast/sass/expression/number.dart
+++ b/lib/src/ast/sass/expression/number.dart
@@ -11,7 +11,7 @@ import '../expression.dart';
/// A number literal.
///
/// {@category AST}
-final class NumberExpression implements Expression {
+final class NumberExpression extends Expression {
/// The numeric value.
final double value;
diff --git a/lib/src/ast/sass/expression/parenthesized.dart b/lib/src/ast/sass/expression/parenthesized.dart
index 3788645e3..3459756a5 100644
--- a/lib/src/ast/sass/expression/parenthesized.dart
+++ b/lib/src/ast/sass/expression/parenthesized.dart
@@ -10,7 +10,7 @@ import '../expression.dart';
/// An expression wrapped in parentheses.
///
/// {@category AST}
-final class ParenthesizedExpression implements Expression {
+final class ParenthesizedExpression extends Expression {
/// The internal expression.
final Expression expression;
diff --git a/lib/src/ast/sass/expression/selector.dart b/lib/src/ast/sass/expression/selector.dart
index 81356690b..85365d84a 100644
--- a/lib/src/ast/sass/expression/selector.dart
+++ b/lib/src/ast/sass/expression/selector.dart
@@ -10,7 +10,7 @@ import '../expression.dart';
/// A parent selector reference, `&`.
///
/// {@category AST}
-final class SelectorExpression implements Expression {
+final class SelectorExpression extends Expression {
final FileSpan span;
SelectorExpression(this.span);
diff --git a/lib/src/ast/sass/expression/string.dart b/lib/src/ast/sass/expression/string.dart
index 2e7824345..f9f05d98e 100644
--- a/lib/src/ast/sass/expression/string.dart
+++ b/lib/src/ast/sass/expression/string.dart
@@ -16,14 +16,15 @@ import '../interpolation.dart';
/// A string literal.
///
/// {@category AST}
-final class StringExpression implements Expression {
+final class StringExpression extends Expression {
/// Interpolation that, when evaluated, produces the contents of this string.
///
- /// Unlike [asInterpolation], escapes are resolved and quotes are not
- /// included.
+ /// If this is a quoted string, escapes are resolved and quotes are not
+ /// included in this text (unlike [asInterpolation]). If it's an unquoted
+ /// string, escapes are *not* resolved.
final Interpolation text;
- /// Whether [this] has quotes.
+ /// Whether `this` has quotes.
final bool hasQuotes;
FileSpan get span => text.span;
@@ -43,7 +44,7 @@ final class StringExpression implements Expression {
/// Returns a string expression with no interpolation.
StringExpression.plain(String text, FileSpan span, {bool quotes = false})
- : text = Interpolation([text], span),
+ : text = Interpolation.plain(text, span),
hasQuotes = quotes;
T accept(ExpressionVisitor visitor) =>
@@ -63,11 +64,12 @@ final class StringExpression implements Expression {
quote ??= _bestQuote(text.contents.whereType());
var buffer = InterpolationBuffer();
buffer.writeCharCode(quote);
- for (var value in text.contents) {
+ for (var i = 0; i < text.contents.length; i++) {
+ var value = text.contents[i];
assert(value is Expression || value is String);
switch (value) {
case Expression():
- buffer.add(value);
+ buffer.add(value, text.spanForElement(i));
case String():
_quoteInnerText(value, quote, buffer, static: static);
}
diff --git a/lib/src/ast/sass/expression/supports.dart b/lib/src/ast/sass/expression/supports.dart
index d5de09a75..142a72e74 100644
--- a/lib/src/ast/sass/expression/supports.dart
+++ b/lib/src/ast/sass/expression/supports.dart
@@ -14,7 +14,7 @@ import '../supports_condition.dart';
/// doesn't include the function name wrapping the condition.
///
/// {@category AST}
-final class SupportsExpression implements Expression {
+final class SupportsExpression extends Expression {
/// The condition itself.
final SupportsCondition condition;
diff --git a/lib/src/ast/sass/expression/unary_operation.dart b/lib/src/ast/sass/expression/unary_operation.dart
index d437fafc2..913d1ef9e 100644
--- a/lib/src/ast/sass/expression/unary_operation.dart
+++ b/lib/src/ast/sass/expression/unary_operation.dart
@@ -13,7 +13,7 @@ import 'list.dart';
/// A unary operator, as in `+$var` or `not fn()`.
///
/// {@category AST}
-final class UnaryOperationExpression implements Expression {
+final class UnaryOperationExpression extends Expression {
/// The operator being invoked.
final UnaryOperator operator;
@@ -63,10 +63,10 @@ enum UnaryOperator {
/// The boolean negation operator, `not`.
not('not', 'not');
- /// The English name of [this].
+ /// The English name of `this`.
final String name;
- /// The Sass syntax for [this].
+ /// The Sass syntax for `this`.
final String operator;
const UnaryOperator(this.name, this.operator);
diff --git a/lib/src/ast/sass/expression/value.dart b/lib/src/ast/sass/expression/value.dart
index 75b01212e..4d2436555 100644
--- a/lib/src/ast/sass/expression/value.dart
+++ b/lib/src/ast/sass/expression/value.dart
@@ -14,7 +14,7 @@ import '../expression.dart';
/// constructed dynamically, as for the `call()` function.
///
/// {@category AST}
-final class ValueExpression implements Expression {
+final class ValueExpression extends Expression {
/// The embedded value.
final Value value;
diff --git a/lib/src/ast/sass/expression/variable.dart b/lib/src/ast/sass/expression/variable.dart
index c07ffbc5a..689e72930 100644
--- a/lib/src/ast/sass/expression/variable.dart
+++ b/lib/src/ast/sass/expression/variable.dart
@@ -12,7 +12,7 @@ import '../reference.dart';
/// A Sass variable.
///
/// {@category AST}
-final class VariableExpression implements Expression, SassReference {
+final class VariableExpression extends Expression implements SassReference {
/// The namespace of the variable being referenced, or `null` if it's
/// referenced without a namespace.
final String? namespace;
@@ -35,5 +35,5 @@ final class VariableExpression implements Expression, SassReference {
T accept(ExpressionVisitor visitor) =>
visitor.visitVariableExpression(this);
- String toString() => namespace == null ? '\$$name' : '$namespace.\$$name';
+ String toString() => span.text;
}
diff --git a/lib/src/ast/sass/interpolation.dart b/lib/src/ast/sass/interpolation.dart
index 075b3344f..345819d21 100644
--- a/lib/src/ast/sass/interpolation.dart
+++ b/lib/src/ast/sass/interpolation.dart
@@ -5,7 +5,6 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
-import '../../interpolation_buffer.dart';
import 'expression.dart';
import 'node.dart';
@@ -19,6 +18,15 @@ final class Interpolation implements SassNode {
/// [String]s.
final List