diff --git a/.github/util/initialize/action.yml b/.github/util/initialize/action.yml index ad2abda5e..ea8c2f0cf 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,7 +13,7 @@ runs: sdk: "${{ inputs.dart-sdk }}" architecture: "${{ inputs.architecture }}" - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "${{ inputs.node-version }}" @@ -23,7 +23,7 @@ runs: - run: npm install shell: bash - - uses: bufbuild/buf-setup-action@v1.13.1 + - uses: bufbuild/buf-setup-action@v1.30.0 with: {github_token: "${{ inputs.github-token }}"} - name: Check out the language repo diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 000000000..e8ccdd7dc --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,75 @@ +name: Build for android + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + lib: lib64 + platform: linux/amd64 + - arch: ia32 + lib: lib + platform: linux/amd64 + - arch: arm64 + lib: lib64 + platform: linux/arm64 + - arch: arm + lib: lib + platform: linux/arm64 + # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. + - arch: riscv64 + lib: lib64 + platform: linux/amd64 # linux/riscv64 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + image: tonistiigi/binfmt:master # need qemu >= 7.0.0 + + - name: Compile Protobuf + run: | + docker run --rm -i \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart <<'EOF' + set -e + curl -fsSL -H "Authorization: Bearer ${{ github.token }}" "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname -s)-$(uname -m).tar.gz" | tar -xzC /usr/local --strip-components 1 + dart pub get + dart run grinder protobuf + EOF + + - name: Build + run: | + docker run --rm -i \ + --platform ${{ matrix.platform }} \ + --privileged \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + ghcr.io/dart-android/dart <<'EOF' + set -e + export DART_SDK=/system/${{ matrix.lib }}/dart + export PATH=$DART_SDK/bin:$PATH + dart pub get + dart run grinder pkg-standalone-android-${{ matrix.arch }} + EOF + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-android-${{ matrix.arch }} + path: build/*.tar.gz + if-no-files-found: error + compression-level: 0 diff --git a/.github/workflows/build-linux-musl.yml b/.github/workflows/build-linux-musl.yml new file mode 100644 index 000000000..5f4ba2639 --- /dev/null +++ b/.github/workflows/build-linux-musl.yml @@ -0,0 +1,69 @@ +name: Build for linux-musl + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + platform: linux/amd64 + - arch: ia32 + platform: linux/386 + - arch: arm64 + platform: linux/arm64 + # There is a bug in qemu's mremap causing pthread_getattr_np in musl to stuck in a loop on arm. + # Unless qemu fixes the bug or we get a real linux-arm runner, we cannot build aot-snapshot + # for arm on CI. So, we create a kernel snapshot for arm build in amd64 container instead. + # https://gitlab.com/qemu-project/qemu/-/issues/1729 + - arch: arm + platform: linux/amd64 # linux/arm/v7 + # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. + - arch: riscv64 + platform: linux/amd64 # linux/riscv64 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Compile Protobuf + run: | + docker run --rm -i \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart <<'EOF' + set -e + curl -fsSL -H "Authorization: Bearer ${{ github.token }}" "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname -s)-$(uname -m).tar.gz" | tar -xzC /usr/local --strip-components 1 + dart pub get + dart run grinder protobuf + EOF + + - name: Build + run: | + docker run --rm -i \ + --platform ${{ matrix.arch == 'arm' && 'linux/amd64' || matrix.platform }} \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + ghcr.io/dart-musl/dart <<'EOF' + set -e + dart pub get + dart run grinder pkg-standalone-linux-${{ matrix.arch }}-musl + EOF + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-linux-musl-${{ matrix.arch }} + path: build/*.tar.gz + if-no-files-found: error + compression-level: 0 diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 000000000..c8f5ddd41 --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,66 @@ +name: Build for linux + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + platform: linux/amd64 + - arch: ia32 + platform: linux/amd64 + - arch: arm + platform: linux/arm/v7 + - arch: arm64 + platform: linux/arm64 + # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. + # https://github.com/dart-lang/dart-docker/issues/96#issuecomment-1669860829 + - arch: riscv64 + platform: linux/amd64 # linux/riscv64 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Compile Protobuf + run: | + docker run --rm -i \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart <<'EOF' + set -e + curl -fsSL -H "Authorization: Bearer ${{ github.token }}" "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname -s)-$(uname -m).tar.gz" | tar -xzC /usr/local --strip-components 1 + dart pub get + dart run grinder protobuf + EOF + + - name: Build + run: | + docker run --rm -i \ + --platform ${{ matrix.platform }} \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart:latest <<'EOF' + set -e + dart pub get + dart run grinder pkg-standalone-linux-${{ matrix.arch }} + EOF + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-linux-${{ matrix.arch }} + 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..3dff23d3b --- /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-14 + + 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..ba9ff8bb1 --- /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-latest + + 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 0e55102c9..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,317 +7,15 @@ on: pull_request: 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 }}"} - - # 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@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 - - - 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@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 - - - 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/. - # - # 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@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 - - 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 - - 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@v4 @@ -336,325 +25,16 @@ jobs: - 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@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 + test_vendor: needs: [double_check] - - 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 - needs: [double_check] - - 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 - needs: [double_check] - - 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.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@v4 - - 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@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - uses: docker/setup-qemu-action@v3 - - 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 - # https://github.blog/2023-10-02-introducing-the-new-apple-silicon-powered-m1-macos-larger-runner-for-github-actions/ - - runner: macos-latest-xlarge - platform: macos-arm64 - architecture: arm64 - - runner: windows-latest - platform: windows - architecture: x64 - - steps: - - uses: actions/checkout@v4 - - 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@v4 - - 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@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 - needs: [bootstrap, bourbon, foundation, bulma] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - 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_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@v4 - - 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@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 - needs: [bootstrap, bourbon, foundation, bulma] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - 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] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - 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: 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_linux, deploy_github_linux_qemu, deploy_github] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - 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 }} + 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..7f7c60139 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,222 @@ +name: Release + +on: + workflow_call: + +jobs: + build_android: + uses: ./.github/workflows/build-android.yml + secrets: inherit + + build_linux: + uses: ./.github/workflows/build-linux.yml + secrets: inherit + + build_linux_musl: + uses: ./.github/workflows/build-linux-musl.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_android, build_linux, build_linux_musl, 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: + 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_sub_packages: + name: Deploy Sub-Packages + 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-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 + + 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..e2d51825e --- /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.sass build/bulma-output.css diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..aa31b610e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,309 @@ +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 + + - 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 + + - 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 + - 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 + - name: Run tests + run: dart run test -p chrome -j 2 + env: + CHROME_EXECUTABLE: chrome diff --git a/CHANGELOG.md b/CHANGELOG.md index ef53f976e..badcd0736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.70.0 +## 1.74.0 * **Breaking change**: Passing a number with unit `%` to the `$alpha` parameter of `color.change()`, `color.adjust()`, `change-color()`, and `adjust-color()` @@ -178,6 +178,159 @@ * Remove `RgbColor`, `HslColor` and `HwbColor` SassScript values. +## 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 diff --git a/bin/sass.dart b/bin/sass.dart index ad23649d4..d24439eb9 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -13,6 +13,7 @@ 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'; @@ -45,6 +46,7 @@ Future main(List args) async { } var graph = StylesheetGraph(ImportCache( + importers: [...options.pkgImporters, FilesystemImporter.noLoadPath], 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 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/style_rule.dart b/lib/src/ast/css/style_rule.dart index ccce74fdb..8b9da6663 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/sass/expression/variable.dart b/lib/src/ast/sass/expression/variable.dart index c07ffbc5a..7a839d867 100644 --- a/lib/src/ast/sass/expression/variable.dart +++ b/lib/src/ast/sass/expression/variable.dart @@ -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/statement/function_rule.dart b/lib/src/ast/sass/statement/function_rule.dart index 9242bf858..885bd4ef9 100644 --- a/lib/src/ast/sass/statement/function_rule.dart +++ b/lib/src/ast/sass/statement/function_rule.dart @@ -6,11 +6,8 @@ import 'package:source_span/source_span.dart'; import '../../../util/span.dart'; import '../../../visitor/interface/statement.dart'; -import '../argument_declaration.dart'; import '../declaration.dart'; -import '../statement.dart'; import 'callable_declaration.dart'; -import 'silent_comment.dart'; /// A function declaration. /// @@ -21,10 +18,8 @@ final class FunctionRule extends CallableDeclaration implements SassDeclaration { FileSpan get nameSpan => span.withoutInitialAtRule().initialIdentifier(); - FunctionRule(String name, ArgumentDeclaration arguments, - Iterable children, FileSpan span, - {SilentComment? comment}) - : super(name, arguments, children, span, comment: comment); + FunctionRule(super.name, super.arguments, super.children, super.span, + {super.comment}); T accept(StatementVisitor visitor) => visitor.visitFunctionRule(this); diff --git a/lib/src/ast/sass/statement/if_rule.dart b/lib/src/ast/sass/statement/if_rule.dart index 2a92ac28c..0e611df12 100644 --- a/lib/src/ast/sass/statement/if_rule.dart +++ b/lib/src/ast/sass/statement/if_rule.dart @@ -94,7 +94,7 @@ final class IfClause extends IfRuleClause { /// /// {@category AST} final class ElseClause extends IfRuleClause { - ElseClause(Iterable children) : super(children); + ElseClause(super.children); String toString() => "@else {${children.join(' ')}}"; } diff --git a/lib/src/ast/sass/statement/mixin_rule.dart b/lib/src/ast/sass/statement/mixin_rule.dart index 624eff53e..650e64b65 100644 --- a/lib/src/ast/sass/statement/mixin_rule.dart +++ b/lib/src/ast/sass/statement/mixin_rule.dart @@ -7,12 +7,9 @@ import 'package:source_span/source_span.dart'; import '../../../util/span.dart'; import '../../../visitor/interface/statement.dart'; import '../../../visitor/statement_search.dart'; -import '../argument_declaration.dart'; import '../declaration.dart'; -import '../statement.dart'; import 'callable_declaration.dart'; import 'content_rule.dart'; -import 'silent_comment.dart'; /// A mixin declaration. /// @@ -31,10 +28,8 @@ final class MixinRule extends CallableDeclaration implements SassDeclaration { return startSpan.initialIdentifier(); } - MixinRule(String name, ArgumentDeclaration arguments, - Iterable children, FileSpan span, - {SilentComment? comment}) - : super(name, arguments, children, span, comment: comment); + MixinRule(super.name, super.arguments, super.children, super.span, + {super.comment}); T accept(StatementVisitor visitor) => visitor.visitMixinRule(this); diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart index 3d97729ca..f6d417901 100644 --- a/lib/src/ast/selector/complex.dart +++ b/lib/src/ast/selector/complex.dart @@ -70,11 +70,10 @@ final class ComplexSelector extends Selector { } ComplexSelector(Iterable> leadingCombinators, - Iterable components, FileSpan span, + Iterable components, super.span, {this.lineBreak = false}) : leadingCombinators = List.unmodifiable(leadingCombinators), - components = List.unmodifiable(components), - super(span) { + components = List.unmodifiable(components) { if (this.leadingCombinators.isEmpty && this.components.isEmpty) { throw ArgumentError( "leadingCombinators and components may not both be empty."); diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart index c36662cb0..bcc2beb33 100644 --- a/lib/src/ast/selector/compound.dart +++ b/lib/src/ast/selector/compound.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../logger.dart'; @@ -43,9 +42,8 @@ final class CompoundSelector extends Selector { SimpleSelector? get singleSimple => components.length == 1 ? components.first : null; - CompoundSelector(Iterable components, FileSpan span) - : components = List.unmodifiable(components), - super(span) { + CompoundSelector(Iterable components, super.span) + : components = List.unmodifiable(components) { if (this.components.isEmpty) { throw ArgumentError("components may not be empty."); } diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index d432bbfaa..8da4598f6 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../exception.dart'; import '../../extend/functions.dart'; @@ -49,9 +48,8 @@ final class SelectorList extends Selector { }), ListSeparator.comma); } - SelectorList(Iterable components, FileSpan span) - : components = List.unmodifiable(components), - super(span) { + SelectorList(Iterable components, super.span) + : components = List.unmodifiable(components) { if (this.components.isEmpty) { throw ArgumentError("components may not be empty."); } @@ -59,9 +57,10 @@ final class SelectorList extends Selector { /// Parses a selector list from [contents]. /// - /// If passed, [url] is the name of the file from which [contents] comes. - /// [allowParent] and [allowPlaceholder] control whether [ParentSelector]s or - /// [PlaceholderSelector]s are allowed in this selector, respectively. + /// If passed, [url] is the name of the file from which [contents] comes. If + /// [allowParent] is false, this doesn't allow [ParentSelector]s. If + /// [plainCss] is true, this parses the selector as plain CSS rather than + /// unresolved Sass. /// /// If passed, [interpolationMap] maps the text of [contents] back to the /// original location of the selector in the source file. @@ -72,13 +71,13 @@ final class SelectorList extends Selector { Logger? logger, InterpolationMap? interpolationMap, bool allowParent = true, - bool allowPlaceholder = true}) => + bool plainCss = false}) => SelectorParser(contents, url: url, logger: logger, interpolationMap: interpolationMap, allowParent: allowParent, - allowPlaceholder: allowPlaceholder) + plainCss: plainCss) .parse(); T accept(SelectorVisitor visitor) => visitor.visitSelectorList(this); @@ -97,17 +96,24 @@ final class SelectorList extends Selector { return contents.isEmpty ? null : SelectorList(contents, span); } - /// Returns a new list with all [ParentSelector]s replaced with [parent]. + /// Returns a new selector list that represents [this] nested within [parent]. /// - /// If [implicitParent] is true, this treats [ComplexSelector]s that don't - /// contain an explicit [ParentSelector] as though they began with one. + /// By default, this replaces [ParentSelector]s in [this] with [parent]. If + /// [preserveParentSelectors] is true, this instead preserves those selectors + /// as parent selectors. + /// + /// If [implicitParent] is true, this prepends [parent] to any + /// [ComplexSelector]s in this that don't contain explicit [ParentSelector]s, + /// or to _all_ [ComplexSelector]s if [preserveParentSelectors] is true. /// /// The given [parent] may be `null`, indicating that this has no parents. If /// so, this list is returned as-is if it doesn't contain any explicit - /// [ParentSelector]s. If it does, this throws a [SassScriptException]. - SelectorList resolveParentSelectors(SelectorList? parent, - {bool implicitParent = true}) { + /// [ParentSelector]s or if [preserveParentSelectors] is true. Otherwise, this + /// throws a [SassScriptException]. + SelectorList nestWithin(SelectorList? parent, + {bool implicitParent = true, bool preserveParentSelectors = false}) { if (parent == null) { + if (preserveParentSelectors) return this; var parentSelector = accept(const _ParentSelectorVisitor()); if (parentSelector == null) return this; throw SassException( @@ -116,7 +122,7 @@ final class SelectorList extends Selector { } return SelectorList(flattenVertically(components.map((complex) { - if (!_containsParentSelector(complex)) { + if (preserveParentSelectors || !_containsParentSelector(complex)) { if (!implicitParent) return [complex]; return parent.components.map((parentComplex) => parentComplex.concatenate(complex, complex.span)); @@ -124,7 +130,7 @@ final class SelectorList extends Selector { var newComplexes = []; for (var component in complex.components) { - var resolved = _resolveParentSelectorsCompound(component, parent); + var resolved = _nestWithinCompound(component, parent); if (resolved == null) { if (newComplexes.isEmpty) { newComplexes.add(ComplexSelector( @@ -167,7 +173,7 @@ final class SelectorList extends Selector { /// [ParentSelector]s replaced with [parent]. /// /// Returns `null` if [component] doesn't contain any [ParentSelector]s. - Iterable? _resolveParentSelectorsCompound( + Iterable? _nestWithinCompound( ComplexSelectorComponent component, SelectorList parent) { var simples = component.selector.components; var containsSelectorPseudo = simples.any((simple) { @@ -183,8 +189,8 @@ final class SelectorList extends Selector { ? simples.map((simple) => switch (simple) { PseudoSelector(:var selector?) when _containsParentSelector(selector) => - simple.withSelector(selector.resolveParentSelectors(parent, - implicitParent: false)), + simple.withSelector( + selector.nestWithin(parent, implicitParent: false)), _ => simple }) : simples; @@ -263,6 +269,8 @@ final class SelectorList extends Selector { /// Returns a copy of `this` with [combinators] added to the end of each /// complex selector in [components]. + /// + /// @nodoc @internal SelectorList withAdditionalCombinators( List> combinators) => diff --git a/lib/src/ast/selector/parent.dart b/lib/src/ast/selector/parent.dart index 18e898652..06f013f05 100644 --- a/lib/src/ast/selector/parent.dart +++ b/lib/src/ast/selector/parent.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -22,7 +21,7 @@ final class ParentSelector extends SimpleSelector { /// indicating that the parent selector will not be modified. final String? suffix; - ParentSelector(FileSpan span, {this.suffix}) : super(span); + ParentSelector(super.span, {this.suffix}); T accept(SelectorVisitor visitor) => visitor.visitParentSelector(this); diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index 0526eed72..d8ae7864d 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../exception.dart'; import '../../logger.dart'; @@ -35,7 +34,7 @@ abstract base class SimpleSelector extends Selector { /// sequence will contain 1000 simple selectors. int get specificity => 1000; - SimpleSelector(FileSpan span) : super(span); + SimpleSelector(super.span); /// Parses a simple selector from [contents]. /// diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart index d714dcb6a..937377164 100644 --- a/lib/src/ast/selector/universal.dart +++ b/lib/src/ast/selector/universal.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../visitor/interface/selector.dart'; @@ -23,7 +22,7 @@ final class UniversalSelector extends SimpleSelector { int get specificity => 0; - UniversalSelector(FileSpan span, {this.namespace}) : super(span); + UniversalSelector(super.span, {this.namespace}); T accept(SelectorVisitor visitor) => visitor.visitUniversalSelector(this); diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index daa2233db..a940d3f26 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -26,7 +26,8 @@ import 'visitor/serialize.dart'; /// Like [compileAsync] in `lib/sass.dart`, but provides more options to support /// the node-sass compatible API and the executable. /// -/// At most one of `importCache` and `nodeImporter` may be provided at once. +/// If both `importCache` and `nodeImporter` are provided, the importers in +/// `importCache` will be evaluated before `nodeImporter`. Future compileAsync(String path, {Syntax? syntax, Logger? logger, @@ -56,7 +57,7 @@ Future compileAsync(String path, (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= AsyncImportCache.none(logger: logger); stylesheet = (await importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), + FilesystemImporter.cwd, p.toUri(canonicalize(path)), originalUrl: p.toUri(path)))!; } else { stylesheet = Stylesheet.parse( @@ -69,7 +70,7 @@ Future compileAsync(String path, logger, importCache, nodeImporter, - FilesystemImporter('.'), + FilesystemImporter.cwd, functions, style, useSpaces, @@ -121,7 +122,7 @@ Future compileStringAsync(String source, logger, importCache, nodeImporter, - importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter('.')), + importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter.cwd), functions, style, useSpaces, diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 9b08e5597..0deb6285f 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -104,6 +104,12 @@ final class AsyncImportCache { : _importers = const [], _logger = logger ?? const Logger.stderr(); + /// Creates an import cache without any globally-available importers, and only + /// the passed in importers. + AsyncImportCache.only(Iterable importers, {Logger? logger}) + : _importers = List.unmodifiable(importers), + _logger = logger ?? const Logger.stderr(); + /// Converts the user's [importers], [loadPaths], and [packageConfig] /// options into a single list of importers. static List _toImporters(Iterable? importers, diff --git a/lib/src/compile.dart b/lib/src/compile.dart index b951c8036..94221405d 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 5178e366228bde7854df12221393857bb3022628 +// Checksum: a9421a2975e79ad591ae32474cd076e1379d0e75 // // ignore_for_file: unused_import @@ -35,7 +35,8 @@ import 'visitor/serialize.dart'; /// Like [compile] in `lib/sass.dart`, but provides more options to support /// the node-sass compatible API and the executable. /// -/// At most one of `importCache` and `nodeImporter` may be provided at once. +/// If both `importCache` and `nodeImporter` are provided, the importers in +/// `importCache` will be evaluated before `nodeImporter`. CompileResult compile(String path, {Syntax? syntax, Logger? logger, @@ -65,7 +66,7 @@ CompileResult compile(String path, (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= ImportCache.none(logger: logger); stylesheet = importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), + FilesystemImporter.cwd, p.toUri(canonicalize(path)), originalUrl: p.toUri(path))!; } else { stylesheet = Stylesheet.parse( @@ -78,7 +79,7 @@ CompileResult compile(String path, logger, importCache, nodeImporter, - FilesystemImporter('.'), + FilesystemImporter.cwd, functions, style, useSpaces, @@ -130,7 +131,7 @@ CompileResult compileString(String source, logger, importCache, nodeImporter, - importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter('.')), + importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter.cwd), functions, style, useSpaces, diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 1a65d236a..2bb069480 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -119,8 +119,7 @@ final class ExplicitConfiguration extends Configuration { /// Creates a base [ExplicitConfiguration] with a [values] map and a /// [nodeWithSpan]. - ExplicitConfiguration(Map values, this.nodeWithSpan) - : super.implicit(values); + ExplicitConfiguration(super.values, this.nodeWithSpan) : super.implicit(); /// Creates an [ExplicitConfiguration] with a [values] map, a [nodeWithSpan] /// and if this is a copy a reference to the [_originalConfiguration]. diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 432b9c041..53415bc14 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -69,11 +69,16 @@ enum Deprecation { deprecatedIn: '1.62.3', description: 'Passing null as alpha in the ${isJS ? 'JS' : 'Dart'} API.'), + fsImporterCwd('fs-importer-cwd', + deprecatedIn: '1.73.0', + description: + 'Using the current working directory as an implicit load path.'), + @Deprecated('This deprecation name was never actually used.') calcInterp('calc-interp', deprecatedIn: null), colorFunctions('color-functions', - deprecatedIn: '1.70.0', + deprecatedIn: '1.74.0', description: 'Using global Sass color functions.'), color4Api('color-4-api', diff --git a/lib/src/embedded/compilation_dispatcher.dart b/lib/src/embedded/compilation_dispatcher.dart index 218ea6f18..356130d84 100644 --- a/lib/src/embedded/compilation_dispatcher.dart +++ b/lib/src/embedded/compilation_dispatcher.dart @@ -11,7 +11,9 @@ import 'package:native_synchronization/mailbox.dart'; import 'package:path/path.dart' as p; import 'package:protobuf/protobuf.dart'; import 'package:sass/sass.dart' as sass; +import 'package:sass/src/importer/node_package.dart' as npi; +import '../logger.dart'; import '../value/function.dart'; import '../value/mixin.dart'; import 'embedded_sass.pb.dart'; @@ -117,8 +119,10 @@ final class CompilationDispatcher { var style = request.style == OutputStyle.COMPRESSED ? sass.OutputStyle.compressed : sass.OutputStyle.expanded; - var logger = EmbeddedLogger(this, - color: request.alertColor, ascii: request.alertAscii); + var logger = request.silent + ? Logger.quiet + : EmbeddedLogger(this, + color: request.alertColor, ascii: request.alertAscii); try { var importers = request.importers.map((importer) => @@ -223,6 +227,10 @@ final class CompilationDispatcher { case InboundMessage_CompileRequest_Importer_Importer.notSet: _checkNoNonCanonicalScheme(importer); return null; + + case InboundMessage_CompileRequest_Importer_Importer.nodePackageImporter: + return npi.NodePackageImporter( + importer.nodePackageImporter.entryPointDirectory); } } diff --git a/lib/src/embedded/importer/file.dart b/lib/src/embedded/importer/file.dart index edd0d3537..57d97ddf9 100644 --- a/lib/src/embedded/importer/file.dart +++ b/lib/src/embedded/importer/file.dart @@ -3,27 +3,19 @@ // https://opensource.org/licenses/MIT. import '../../importer.dart'; -import '../compilation_dispatcher.dart'; import '../embedded_sass.pb.dart' hide SourceSpan; import 'base.dart'; -/// A filesystem importer to use for most implementation details of -/// [FileImporter]. -/// -/// This allows us to avoid duplicating logic between the two importers. -final _filesystemImporter = FilesystemImporter('.'); - /// An importer that asks the host to resolve imports in a simplified, /// file-system-centric way. final class FileImporter extends ImporterBase { /// The host-provided ID of the importer to invoke. final int _importerId; - FileImporter(CompilationDispatcher dispatcher, this._importerId) - : super(dispatcher); + FileImporter(super.dispatcher, this._importerId); Uri? canonicalize(Uri url) { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); var request = OutboundMessage_FileImportRequest() ..importerId = _importerId @@ -41,7 +33,7 @@ final class FileImporter extends ImporterBase { throw 'The file importer must return a file: URL, was "$url"'; } - return _filesystemImporter.canonicalize(url); + return FilesystemImporter.cwd.canonicalize(url); case InboundMessage_FileImportResponse_Result.error: throw response.error; @@ -51,7 +43,7 @@ final class FileImporter extends ImporterBase { } } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); bool isNonCanonicalScheme(String scheme) => scheme != 'file'; diff --git a/lib/src/embedded/importer/host.dart b/lib/src/embedded/importer/host.dart index 66e60848a..25245721b 100644 --- a/lib/src/embedded/importer/host.dart +++ b/lib/src/embedded/importer/host.dart @@ -6,7 +6,6 @@ import '../../exception.dart'; import '../../importer.dart'; import '../../importer/utils.dart'; import '../../util/span.dart'; -import '../compilation_dispatcher.dart'; import '../embedded_sass.pb.dart' hide SourceSpan; import '../utils.dart'; import 'base.dart'; @@ -20,10 +19,9 @@ final class HostImporter extends ImporterBase { /// [canonicalize]. final Set _nonCanonicalSchemes; - HostImporter(CompilationDispatcher dispatcher, this._importerId, - Iterable nonCanonicalSchemes) - : _nonCanonicalSchemes = Set.unmodifiable(nonCanonicalSchemes), - super(dispatcher) { + HostImporter( + super.dispatcher, this._importerId, Iterable nonCanonicalSchemes) + : _nonCanonicalSchemes = Set.unmodifiable(nonCanonicalSchemes) { for (var scheme in _nonCanonicalSchemes) { if (isValidUrlScheme(scheme)) continue; throw SassException( diff --git a/lib/src/embedded/isolate_dispatcher.dart b/lib/src/embedded/isolate_dispatcher.dart index 018321cdd..b208cc6b3 100644 --- a/lib/src/embedded/isolate_dispatcher.dart +++ b/lib/src/embedded/isolate_dispatcher.dart @@ -137,7 +137,7 @@ class IsolateDispatcher { ..protocolVersion = const String.fromEnvironment("protocol-version") ..compilerVersion = const String.fromEnvironment("compiler-version") ..implementationVersion = const String.fromEnvironment("compiler-version") - ..implementationName = "Dart Sass"; + ..implementationName = "dart-sass"; } /// Handles an error thrown by the dispatcher or code it dispatches to. diff --git a/lib/src/evaluation_context.dart b/lib/src/evaluation_context.dart index 9ae8f1380..1f831cd87 100644 --- a/lib/src/evaluation_context.dart +++ b/lib/src/evaluation_context.dart @@ -18,13 +18,16 @@ abstract interface class EvaluationContext { /// /// Throws [StateError] if there isn't a Sass stylesheet currently being /// evaluated. - static EvaluationContext get current { - if (Zone.current[#_evaluationContext] case EvaluationContext context) { - return context; - } else { - throw StateError("No Sass stylesheet is currently being evaluated."); - } - } + static EvaluationContext get current => + currentOrNull ?? + (throw StateError("No Sass stylesheet is currently being evaluated.")); + + /// The current evaluation context, or `null` if none exists. + static EvaluationContext? get currentOrNull => + switch (Zone.current[#_evaluationContext]) { + EvaluationContext context => context, + _ => null + }; /// The current evaluation context, or null if there isn't a Sass stylesheet /// currently being evaluated. @@ -61,13 +64,20 @@ abstract interface class EvaluationContext { /// This may only be called within a custom function or importer callback. /// {@category Compile} void warn(String message, {bool deprecation = false}) => - EvaluationContext.current - .warn(message, deprecation ? Deprecation.userAuthored : null); + switch (EvaluationContext.currentOrNull) { + var context? => + context.warn(message, deprecation ? Deprecation.userAuthored : null), + _ when deprecation => (const Logger.stderr()) + .warnForDeprecation(Deprecation.userAuthored, message), + _ => (const Logger.stderr()).warn(message) + }; /// Prints a deprecation warning with [message] of type [deprecation]. -void warnForDeprecation(String message, Deprecation deprecation) { - EvaluationContext.current.warn(message, deprecation); -} +void warnForDeprecation(String message, Deprecation deprecation) => + switch (EvaluationContext.currentOrNull) { + var context? => context.warn(message, deprecation), + _ => (const Logger.stderr()).warnForDeprecation(deprecation, message) + }; /// Prints a deprecation warning with [message] of type [deprecation], /// using stderr if there is no [EvaluationContext.current]. diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 38a1a057e..898258c88 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -28,10 +28,9 @@ class SassException extends SourceSpanException { /// compilation, before it failed. final Set loadedUrls; - SassException(String message, FileSpan span, [Iterable? loadedUrls]) + SassException(super.message, FileSpan super.span, [Iterable? loadedUrls]) : loadedUrls = - loadedUrls == null ? const {} : Set.unmodifiable(loadedUrls), - super(message, span); + loadedUrls == null ? const {} : Set.unmodifiable(loadedUrls); /// Converts this to a [MultiSpanSassException] with the additional [span] and /// [label]. @@ -83,12 +82,12 @@ class SassException extends SourceSpanException { .replaceAll("\r\n", "\n"); term_glyph.ascii = wasAscii; - // For the string comment, render all non-ASCII characters as escape + // For the string comment, render all non-US-ASCII characters as escape // sequences so that they'll show up even if the HTTP headers are set // incorrectly. var stringMessage = StringBuffer(); for (var rune in SassString(toString(color: false)).toString().runes) { - if (rune > 0xFF) { + if (rune > 0x7F) { stringMessage ..writeCharCode($backslash) ..write(rune.toRadixString(16)) @@ -224,9 +223,7 @@ class SassFormatException extends SassException SassFormatException withLoadedUrls(Iterable loadedUrls) => SassFormatException(message, span, loadedUrls); - SassFormatException(String message, FileSpan span, - [Iterable? loadedUrls]) - : super(message, span, loadedUrls); + SassFormatException(super.message, super.span, [super.loadedUrls]); } /// A [SassFormatException] that's also a [MultiSpanFormatException]. @@ -248,10 +245,9 @@ class MultiSpanSassFormatException extends MultiSpanSassException MultiSpanSassFormatException( message, span, primaryLabel, secondarySpans, loadedUrls); - MultiSpanSassFormatException(String message, FileSpan span, - String primaryLabel, Map secondarySpans, - [Iterable? loadedUrls]) - : super(message, span, primaryLabel, secondarySpans, loadedUrls); + MultiSpanSassFormatException( + super.message, super.span, super.primaryLabel, super.secondarySpans, + [super.loadedUrls]); } /// An exception thrown by SassScript. @@ -287,9 +283,8 @@ class MultiSpanSassScriptException extends SassScriptException { final Map secondarySpans; MultiSpanSassScriptException( - String message, this.primaryLabel, Map secondarySpans) - : secondarySpans = Map.unmodifiable(secondarySpans), - super(message); + super.message, this.primaryLabel, Map secondarySpans) + : secondarySpans = Map.unmodifiable(secondarySpans); /// Converts this to a [SassException] with the given primary [span]. MultiSpanSassException withSpan(FileSpan span) => diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 70b52ba10..cd121a6f5 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -68,13 +68,13 @@ Future<(int, String, String?)?> compileStylesheet(ExecutableOptions options, Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, StylesheetGraph graph, String? source, String? destination, {bool ifModified = false}) async { - var importer = FilesystemImporter('.'); + var importer = FilesystemImporter.cwd; if (ifModified) { try { if (source != null && destination != null && - !graph.modifiedSince( - p.toUri(source), modificationTime(destination), importer)) { + !graph.modifiedSince(p.toUri(p.absolute(source)), + modificationTime(destination), importer)) { return; } } on FileSystemException catch (_) { @@ -95,14 +95,16 @@ Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, try { if (options.asynchronous) { var importCache = AsyncImportCache( - loadPaths: options.loadPaths, logger: options.logger); + importers: options.pkgImporters, + loadPaths: options.loadPaths, + logger: options.logger); result = source == null ? await compileStringAsync(await readStdin(), syntax: syntax, logger: options.logger, importCache: importCache, - importer: FilesystemImporter('.'), + importer: FilesystemImporter.cwd, style: options.style, quietDeps: options.quietDeps, verbose: options.verbose, @@ -127,7 +129,7 @@ Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, syntax: syntax, logger: options.logger, importCache: graph.importCache, - importer: FilesystemImporter('.'), + importer: FilesystemImporter.cwd, style: options.style, quietDeps: options.quietDeps, verbose: options.verbose, diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 504999c49..876b55814 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -10,6 +10,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:term_glyph/term_glyph.dart' as term_glyph; import '../../sass.dart'; +import '../importer/node_package.dart'; import '../io.dart'; import '../util/character.dart'; @@ -47,6 +48,12 @@ final class ExecutableOptions { help: 'A path to use when resolving imports.\n' 'May be passed multiple times.', splitCommas: false) + ..addMultiOption('pkg-importer', + abbr: 'p', + valueHelp: 'TYPE', + allowed: ['node'], + help: 'Built-in importer(s) to use for pkg: URLs.', + allowedHelp: {'node': 'Load files like Node.js package resolution.'}) ..addOption('style', abbr: 's', valueHelp: 'NAME', @@ -218,6 +225,12 @@ final class ExecutableOptions { /// The set of paths Sass in which should look for imported files. List get loadPaths => _options['load-path'] as List; + /// The list of built-in importers to use to load `pkg:` URLs. + List get pkgImporters => [ + for (var _ in _options['pkg-importer'] as List) + NodePackageImporter('.') + ]; + /// Whether to run the evaluator in asynchronous mode, for debugging purposes. bool get asynchronous => _options['async'] as bool; diff --git a/lib/src/executable/repl.dart b/lib/src/executable/repl.dart index d460b40e0..e2e858a26 100644 --- a/lib/src/executable/repl.dart +++ b/lib/src/executable/repl.dart @@ -22,8 +22,11 @@ Future repl(ExecutableOptions options) async { var repl = Repl(prompt: '>> '); var logger = TrackingLogger(options.logger); var evaluator = Evaluator( - importer: FilesystemImporter('.'), - importCache: ImportCache(loadPaths: options.loadPaths, logger: logger), + importer: FilesystemImporter.cwd, + importCache: ImportCache( + importers: options.pkgImporters, + loadPaths: options.loadPaths, + logger: logger), logger: logger); await for (String line in repl.runAsync()) { if (line.trim().isEmpty) continue; diff --git a/lib/src/executable/watch.dart b/lib/src/executable/watch.dart index c8a222b0b..9e1db78e9 100644 --- a/lib/src/executable/watch.dart +++ b/lib/src/executable/watch.dart @@ -39,7 +39,7 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async { var sourcesToDestinations = _sourcesToDestinations(options); for (var source in sourcesToDestinations.keys) { graph.addCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(source)), p.toUri(source), + FilesystemImporter.cwd, p.toUri(canonicalize(source)), p.toUri(source), recanonicalize: false); } var success = await compileStylesheets(options, graph, sourcesToDestinations, @@ -130,7 +130,7 @@ final class _Watcher { await compileStylesheets(_options, _graph, {path: destination}, ifModified: true); var downstream = _graph.addCanonical( - FilesystemImporter('.'), _canonicalize(path), p.toUri(path)); + FilesystemImporter.cwd, _canonicalize(path), p.toUri(path)); return await _recompileDownstream(downstream) && success; } @@ -144,7 +144,7 @@ final class _Watcher { if (_destinationFor(path) case var destination?) _delete(destination); } - var downstream = _graph.remove(FilesystemImporter('.'), url); + var downstream = _graph.remove(FilesystemImporter.cwd, url); return await _recompileDownstream(downstream); } diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index 6299c5fcf..01d70d248 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -9,6 +9,7 @@ /// aren't instance methods on other objects because their APIs aren't a good /// fit—usually because they deal with raw component lists rather than selector /// classes, to reduce allocations. +library; import 'dart:collection'; diff --git a/lib/src/functions/selector.dart b/lib/src/functions/selector.dart index d6fddf9de..08e133dbf 100644 --- a/lib/src/functions/selector.dart +++ b/lib/src/functions/selector.dart @@ -52,7 +52,7 @@ final _nest = _function("nest", r"$selectors...", (arguments) { first = false; return result; }) - .reduce((parent, child) => child.resolveParentSelectors(parent)) + .reduce((parent, child) => child.nestWithin(parent)) .asSassList; }); @@ -83,7 +83,7 @@ final _append = _function("append", r"$selectors...", (arguments) { ...rest ], span); }), span) - .resolveParentSelectors(parent); + .nestWithin(parent); }).asSassList; }); diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 397e676aa..e34f0a7ee 100644 --- a/lib/src/import_cache.dart +++ b/lib/src/import_cache.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_import_cache.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 342e907cf10e1dd80d7045fc32db43c74376654e +// Checksum: d157b83599dbc07a80ac6cb5ffdf5dde03b60376 // // ignore_for_file: unused_import @@ -106,6 +106,12 @@ final class ImportCache { : _importers = const [], _logger = logger ?? const Logger.stderr(); + /// Creates an import cache without any globally-available importers, and only + /// the passed in importers. + ImportCache.only(Iterable importers, {Logger? logger}) + : _importers = List.unmodifiable(importers), + _logger = logger ?? const Logger.stderr(); + /// Converts the user's [importers], [loadPaths], and [packageConfig] /// options into a single list of importers. static List _toImporters(Iterable? importers, diff --git a/lib/src/importer/filesystem.dart b/lib/src/importer/filesystem.dart index 31af69829..cb23d3095 100644 --- a/lib/src/importer/filesystem.dart +++ b/lib/src/importer/filesystem.dart @@ -5,27 +5,86 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import '../deprecation.dart'; +import '../evaluation_context.dart'; import '../importer.dart'; import '../io.dart' as io; import '../syntax.dart'; import '../util/nullable.dart'; import 'utils.dart'; -/// An importer that loads files from a load path on the filesystem. +/// An importer that loads files from a load path on the filesystem, either +/// relative to the path passed to [FilesystemImporter.new] or absolute `file:` +/// URLs. +/// +/// Use [FilesystemImporter.noLoadPath] to _only_ load absolute `file:` URLs and +/// URLs relative to the current file. /// /// {@category Importer} @sealed class FilesystemImporter extends Importer { /// The path relative to which this importer looks for files. - final String _loadPath; + /// + /// If this is `null`, this importer will _only_ load absolute `file:` URLs + /// and URLs relative to the current file. + final String? _loadPath; + + /// Whether loading from files from this importer's [_loadPath] is deprecated. + final bool _loadPathDeprecated; /// Creates an importer that loads files relative to [loadPath]. - FilesystemImporter(String loadPath) : _loadPath = p.absolute(loadPath); + FilesystemImporter(String loadPath) + : _loadPath = p.absolute(loadPath), + _loadPathDeprecated = false; + + FilesystemImporter._deprecated(String loadPath) + : _loadPath = p.absolute(loadPath), + _loadPathDeprecated = true; + + /// Creates an importer that _only_ loads absolute `file:` URLs and URLs + /// relative to the current file. + FilesystemImporter._noLoadPath() + : _loadPath = null, + _loadPathDeprecated = false; + + /// A [FilesystemImporter] that loads files relative to the current working + /// directory. + /// + /// Historically, this was the best default for supporting `file:` URL loads + /// when the load path didn't matter. However, adding the current working + /// directory to the load path wasn't always desirable, so it's no longer + /// recommended. Instead, either use [FilesystemImporter.noLoadPath] if the + /// load path doesn't matter, or `FilesystemImporter('.')` to explicitly + /// preserve the existing behavior. + @Deprecated( + "Use FilesystemImporter.noLoadPath or FilesystemImporter('.') instead.") + static final cwd = FilesystemImporter._deprecated('.'); + + /// Creates an importer that _only_ loads absolute `file:` URLsand URLs + /// relative to the current file. + static final noLoadPath = FilesystemImporter._noLoadPath(); Uri? canonicalize(Uri url) { - if (url.scheme != 'file' && url.scheme != '') return null; - return resolveImportPath(p.join(_loadPath, p.fromUri(url))) - .andThen((resolved) => p.toUri(io.canonicalize(resolved))); + String? resolved; + if (url.scheme == 'file') { + resolved = resolveImportPath(p.fromUri(url)); + } else if (url.scheme != '') { + return null; + } else if (_loadPath case var loadPath?) { + resolved = resolveImportPath(p.join(loadPath, p.fromUri(url))); + + if (resolved != null && _loadPathDeprecated) { + warnForDeprecation( + "Using the current working directory as an implicit load path is " + "deprecated. Either add it as an explicit load path or importer, or " + "load this stylesheet from a different URL.", + Deprecation.fsImporterCwd); + } + } else { + return null; + } + + return resolved.andThen((resolved) => p.toUri(io.canonicalize(resolved))); } ImporterResult? load(Uri url) { @@ -50,5 +109,5 @@ class FilesystemImporter extends Importer { basename == p.url.withoutExtension(canonicalBasename); } - String toString() => _loadPath; + String toString() => _loadPath ?? ''; } diff --git a/lib/src/importer/js_to_dart/async_file.dart b/lib/src/importer/js_to_dart/async_file.dart index e984531dc..7be4b9461 100644 --- a/lib/src/importer/js_to_dart/async_file.dart +++ b/lib/src/importer/js_to_dart/async_file.dart @@ -17,12 +17,6 @@ import '../filesystem.dart'; import '../result.dart'; import '../utils.dart'; -/// A filesystem importer to use for most implementation details of -/// [JSToDartAsyncFileImporter]. -/// -/// This allows us to avoid duplicating logic between the two importers. -final _filesystemImporter = FilesystemImporter('.'); - /// A wrapper for a potentially-asynchronous JS API file importer that exposes /// it as a Dart [AsyncImporter]. final class JSToDartAsyncFileImporter extends AsyncImporter { @@ -32,7 +26,7 @@ final class JSToDartAsyncFileImporter extends AsyncImporter { JSToDartAsyncFileImporter(this._findFileUrl); FutureOr canonicalize(Uri url) async { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); var result = wrapJSExceptions(() => _findFileUrl( url.toString(), @@ -52,16 +46,16 @@ final class JSToDartAsyncFileImporter extends AsyncImporter { '"$url".')); } - return _filesystemImporter.canonicalize(resultUrl); + return FilesystemImporter.cwd.canonicalize(resultUrl); } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); DateTime modificationTime(Uri url) => - _filesystemImporter.modificationTime(url); + FilesystemImporter.cwd.modificationTime(url); bool couldCanonicalize(Uri url, Uri canonicalUrl) => - _filesystemImporter.couldCanonicalize(url, canonicalUrl); + FilesystemImporter.cwd.couldCanonicalize(url, canonicalUrl); bool isNonCanonicalScheme(String scheme) => scheme != 'file'; } diff --git a/lib/src/importer/js_to_dart/file.dart b/lib/src/importer/js_to_dart/file.dart index 9ad474d00..e3302f881 100644 --- a/lib/src/importer/js_to_dart/file.dart +++ b/lib/src/importer/js_to_dart/file.dart @@ -12,12 +12,6 @@ import '../../js/utils.dart'; import '../../util/nullable.dart'; import '../utils.dart'; -/// A filesystem importer to use for most implementation details of -/// [JSToDartAsyncFileImporter]. -/// -/// This allows us to avoid duplicating logic between the two importers. -final _filesystemImporter = FilesystemImporter('.'); - /// A wrapper for a potentially-asynchronous JS API file importer that exposes /// it as a Dart [AsyncImporter]. final class JSToDartFileImporter extends Importer { @@ -27,7 +21,7 @@ final class JSToDartFileImporter extends Importer { JSToDartFileImporter(this._findFileUrl); Uri? canonicalize(Uri url) { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); var result = wrapJSExceptions(() => _findFileUrl( url.toString(), @@ -51,16 +45,16 @@ final class JSToDartFileImporter extends Importer { '"$url".')); } - return _filesystemImporter.canonicalize(resultUrl); + return FilesystemImporter.cwd.canonicalize(resultUrl); } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); DateTime modificationTime(Uri url) => - _filesystemImporter.modificationTime(url); + FilesystemImporter.cwd.modificationTime(url); bool couldCanonicalize(Uri url, Uri canonicalUrl) => - _filesystemImporter.couldCanonicalize(url, canonicalUrl); + FilesystemImporter.cwd.couldCanonicalize(url, canonicalUrl); bool isNonCanonicalScheme(String scheme) => scheme != 'file'; } diff --git a/lib/src/importer/node_package.dart b/lib/src/importer/node_package.dart new file mode 100644 index 000000000..d621ccf94 --- /dev/null +++ b/lib/src/importer/node_package.dart @@ -0,0 +1,384 @@ +// Copyright 2024 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:cli_pkg/js.dart'; +import 'package:collection/collection.dart'; +import 'package:sass/src/util/map.dart'; +import 'package:sass/src/util/nullable.dart'; + +import '../importer.dart'; +import './utils.dart'; +import 'dart:convert'; +import '../io.dart'; +import 'package:path/path.dart' as p; + +/// An [Importer] that resolves `pkg:` URLs using the Node resolution algorithm. +class NodePackageImporter extends Importer { + /// The starting path for canonicalizations without a containing URL. + late final String _entryPointDirectory; + + /// Creates a Node package importer with the associated entry point. + NodePackageImporter(String entryPointDirectory) { + if (isBrowser) { + throw "The Node package importer cannot be used without a filesystem."; + } + _entryPointDirectory = p.absolute(entryPointDirectory); + } + + @override + bool isNonCanonicalScheme(String scheme) => scheme == 'pkg'; + + @override + Uri? canonicalize(Uri url) { + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); + if (url.scheme != 'pkg') return null; + + if (url.hasAuthority) { + throw "A pkg: URL must not have a host, port, username or password."; + } else if (p.url.isAbsolute(url.path)) { + throw "A pkg: URL's path must not begin with /."; + } else if (url.path.isEmpty) { + throw "A pkg: URL must not have an empty path."; + } else if (url.hasQuery || url.hasFragment) { + throw "A pkg: URL must not have a query or fragment."; + } + + var baseDirectory = containingUrl?.scheme == 'file' + ? p.dirname(p.fromUri(containingUrl!)) + : _entryPointDirectory; + + var (packageName, subpath) = _packageNameAndSubpath(url.path); + + // If the package name is not a valid Node package name, return null in case + // another importer can handle. + if (packageName.startsWith('.') || + packageName.contains('\\') || + packageName.contains('%') || + (packageName.startsWith('@') && + !packageName.contains(p.url.separator))) { + return null; + } + + var packageRoot = _resolvePackageRoot(packageName, baseDirectory); + + if (packageRoot == null) return null; + var jsonPath = p.join(packageRoot, 'package.json'); + + var jsonString = readFile(jsonPath); + Map packageManifest; + try { + packageManifest = json.decode(jsonString) as Map; + } catch (e) { + throw "Failed to parse $jsonPath for \"pkg:$packageName\": $e"; + } + + if (_resolvePackageExports( + packageRoot, subpath, packageManifest, packageName) + case var resolved?) { + if (_validExtensions.contains(p.extension(resolved))) { + return p.toUri(p.canonicalize(resolved)); + } else { + throw "The export for '${subpath ?? "root"}' in " + "'$packageName' resolved to '${resolved.toString()}', " + "which is not a '.scss', '.sass', or '.css' file."; + } + } + // If no subpath, attempt to resolve `sass` or `style` key in package.json, + // then `index` file at package root, resolved for file extensions and + // partials. + if (subpath == null) { + var rootPath = _resolvePackageRootValues(packageRoot, packageManifest); + return rootPath != null ? p.toUri(p.canonicalize(rootPath)) : null; + } + + // If there is a subpath, attempt to resolve the path relative to the + // package root, and resolve for file extensions and partials. + var subpathInRoot = p.join(packageRoot, subpath); + return FilesystemImporter.cwd.canonicalize(p.toUri(subpathInRoot)); + } + + @override + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); + + /// Splits a [bare import + /// specifier](https://nodejs.org/api/esm.html#import-specifiers) `specifier` + /// into its package name and subpath, if one exists. + /// + /// Because this is a bare import specifier and not a path, we always use `/` + /// to avoid invalid values on non-Posix machines. + (String, String?) _packageNameAndSubpath(String specifier) { + var parts = p.url.split(specifier); + var name = p.fromUri(parts.removeAt(0)); + + if (name.startsWith('@')) { + if (parts.isNotEmpty) name = p.url.join(name, parts.removeAt(0)); + } + var subpath = parts.isNotEmpty ? p.fromUri(p.url.joinAll(parts)) : null; + return (name, subpath); + } + + /// Returns an absolute path to the root directory for the most proximate + /// installed `packageName`. + /// + /// Implementation of `PACKAGE_RESOLVE` from the [Resolution Algorithm + /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification). + String? _resolvePackageRoot(String packageName, String baseDirectory) { + while (true) { + var potentialPackage = p.join(baseDirectory, 'node_modules', packageName); + if (dirExists(potentialPackage)) return potentialPackage; + // baseDirectory has now reached root without finding a match. + if (p.split(baseDirectory).length == 1) return null; + baseDirectory = p.dirname(baseDirectory); + } + } + + /// Returns a file path specified by the `sass` or `style` values in a package + /// manifest, or an `index` file relative to the package root. + String? _resolvePackageRootValues( + String packageRoot, Map packageManifest) { + if (packageManifest['sass'] case String sassValue + when _validExtensions.contains(p.url.extension(sassValue))) { + return p.join(packageRoot, sassValue); + } else if (packageManifest['style'] case String styleValue + when _validExtensions.contains(p.url.extension(styleValue))) { + return p.join(packageRoot, styleValue); + } + + var result = resolveImportPath(p.join(packageRoot, 'index')); + return result; + } + + /// Returns a file path specified by a `subpath` in the `exports` section of + /// package.json. + /// + /// `packageName` is used for error reporting. + String? _resolvePackageExports(String packageRoot, String? subpath, + Map packageManifest, String packageName) { + var exports = packageManifest['exports'] as Object?; + if (exports == null) return null; + var subpathVariants = _exportsToCheck(subpath); + if (_nodePackageExportsResolve( + packageRoot, subpathVariants, exports, subpath, packageName) + case var path?) { + return path; + } + + if (subpath != null && p.url.extension(subpath).isNotEmpty) return null; + + var subpathIndexVariants = _exportsToCheck(subpath, addIndex: true); + if (_nodePackageExportsResolve( + packageRoot, subpathIndexVariants, exports, subpath, packageName) + case var path?) { + return path; + } + + return null; + } + + /// Returns the path to one subpath variant, resolved in the `exports` of a + /// package manifest. + /// + /// Throws an error if multiple `subpathVariants` match, and null if none + /// match. + /// + /// Implementation of `PACKAGE_EXPORTS_RESOLVE` from the [Resolution Algorithm + /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification). + String? _nodePackageExportsResolve( + String packageRoot, + List subpathVariants, + Object exports, + String? subpath, + String packageName) { + if (exports is Map && + exports.keys.any((key) => key.startsWith('.')) && + exports.keys.any((key) => !key.startsWith('.'))) { + throw '`exports` in $packageName can not have both conditions and paths ' + 'at the same level.\n' + 'Found ${exports.keys.map((key) => '"$key"').join(',')} in ' + '${p.join(packageRoot, 'package.json')}.'; + } + + var matches = subpathVariants + .map((String? variant) { + if (variant == null) { + return _getMainExport(exports).andThen((mainExport) => + _packageTargetResolve(variant, mainExport, packageRoot)); + } else if (exports is! Map || + exports.keys.every((key) => !key.startsWith('.'))) { + return null; + } + var matchKey = "./${p.toUri(variant)}"; + if (exports.containsKey(matchKey) && + exports[matchKey] != null && + !matchKey.contains('*')) { + return _packageTargetResolve( + matchKey, exports[matchKey] as Object, packageRoot); + } + + var expansionKeys = [ + for (var key in exports.keys) + if ('*'.allMatches(key).length == 1) key + ]..sort(_compareExpansionKeys); + + for (var expansionKey in expansionKeys) { + var [patternBase, patternTrailer] = expansionKey.split('*'); + if (!matchKey.startsWith(patternBase)) continue; + if (matchKey == patternBase) continue; + if (patternTrailer.isEmpty || + (matchKey.endsWith(patternTrailer) && + matchKey.length >= expansionKey.length)) { + var target = exports[expansionKey] as Object?; + if (target == null) continue; + var patternMatch = matchKey.substring( + patternBase.length, matchKey.length - patternTrailer.length); + return _packageTargetResolve( + variant, target, packageRoot, patternMatch); + } + } + + return null; + }) + .whereNotNull() + .toList(); + + return switch (matches) { + [var path] => path, + [] => null, + var paths => + throw "Unable to determine which of multiple potential resolutions " + "found for ${subpath ?? 'root'} in $packageName should be used. " + "\n\nFound:\n" + "${paths.join('\n')}" + }; + } + + /// Implementation of the `PATTERN_KEY_COMPARE` comparator from + /// https://nodejs.org/api/esm.html#resolution-algorithm-specification. + int _compareExpansionKeys(String keyA, String keyB) { + var baseLengthA = keyA.contains('*') ? keyA.indexOf('*') + 1 : keyA.length; + var baseLengthB = keyB.contains('*') ? keyB.indexOf('*') + 1 : keyB.length; + if (baseLengthA > baseLengthB) return -1; + if (baseLengthB > baseLengthA) return 1; + if (!keyA.contains("*")) return 1; + if (!keyB.contains("*")) return -1; + if (keyA.length > keyB.length) return -1; + if (keyB.length > keyA.length) return 1; + return 0; + } + + /// Returns a file path for `subpath`, as resolved in the `exports` object. + /// + /// Verifies the file exists relative to `packageRoot`. Instances of `*` will + /// be replaced with `patternMatch`. + /// + /// `subpath` and `packageRoot` are native paths, and `patternMatch` is a URL + /// path. + /// + /// Implementation of `PACKAGE_TARGET_RESOLVE` from the [Resolution Algorithm + /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification). + String? _packageTargetResolve( + String? subpath, Object exports, String packageRoot, + [String? patternMatch]) { + switch (exports) { + case String string when !string.startsWith('./'): + throw "Export '$string' must be a path relative to the package root at '$packageRoot'."; + case String string when patternMatch != null: + var replaced = p.fromUri(string.replaceFirst('*', patternMatch)); + var path = p.normalize(p.join(packageRoot, replaced)); + return fileExists(path) ? path : null; + case String string: + return p.join(packageRoot, p.fromUri(string)); + case Map map: + for (var (key, value) in map.pairs) { + if (!const {'sass', 'style', 'default'}.contains(key)) continue; + if (value == null) continue; + if (_packageTargetResolve( + subpath, value as Object, packageRoot, patternMatch) + case var result?) { + return result; + } + } + return null; + + case []: + return null; + + case List array: + for (var value in array) { + if (value == null) continue; + if (_packageTargetResolve( + subpath, value as Object, packageRoot, patternMatch) + case var result?) { + return result; + } + } + + return null; + + default: + throw "Invalid 'exports' value $exports in " + "${p.join(packageRoot, 'package.json')}."; + } + } + + /// Returns a path to a package's export without a subpath. + Object? _getMainExport(Object exports) { + return switch (exports) { + String string => string, + List list => list, + Map map + when !map.keys.any((key) => key.startsWith('.')) => + map, + {'.': var export?} => export, + _ => null + }; + } + + /// Returns a list of all possible variations of `subpath` with extensions and + /// partials. + /// + /// If there is no subpath, returns a single `null` value, which is used in + /// `_nodePackageExportsResolve` to denote the main package export. + List _exportsToCheck(String? subpath, {bool addIndex = false}) { + var paths = []; + + if (subpath == null && addIndex) { + subpath = 'index'; + } else if (subpath != null && addIndex) { + subpath = p.join(subpath, 'index'); + } + if (subpath == null) return [null]; + + if (_validExtensions.contains(p.url.extension(subpath))) { + paths.add(subpath); + } else { + paths.addAll([ + subpath, + '$subpath.scss', + '$subpath.sass', + '$subpath.css', + ]); + } + var basename = p.basename(subpath); + var dirname = p.dirname(subpath); + + if (basename.startsWith('_')) return paths; + + return [ + ...paths, + for (var path in paths) + if (dirname == '.') + '_${p.basename(path)}' + else + p.join(dirname, '_${p.basename(path)}') + ]; + } +} + +/// The set of file extensions that Sass can parse. +/// +/// `NodePackageImporter` will only resolve files with these extensions, and +/// uses these extensions to check for matches if no extension is provided in +/// the Url to canonicalize. +const _validExtensions = {'.scss', '.sass', '.css'}; diff --git a/lib/src/importer/package.dart b/lib/src/importer/package.dart index 21f41509f..39d09ac63 100644 --- a/lib/src/importer/package.dart +++ b/lib/src/importer/package.dart @@ -7,12 +7,6 @@ import 'package:package_config/package_config_types.dart'; import '../importer.dart'; -/// A filesystem importer to use when resolving the results of `package:` URLs. -/// -/// This allows us to avoid duplicating the logic for choosing an extension and -/// looking for partials. -final _filesystemImporter = FilesystemImporter('.'); - /// An importer that loads stylesheets from `package:` imports. /// /// {@category Importer} @@ -29,7 +23,7 @@ class PackageImporter extends Importer { PackageImporter(PackageConfig packageConfig) : _packageConfig = packageConfig; Uri? canonicalize(Uri url) { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); if (url.scheme != 'package') return null; var resolved = _packageConfig.resolve(url); @@ -39,17 +33,18 @@ class PackageImporter extends Importer { throw "Unsupported URL $resolved."; } - return _filesystemImporter.canonicalize(resolved); + return FilesystemImporter.cwd.canonicalize(resolved); } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); DateTime modificationTime(Uri url) => - _filesystemImporter.modificationTime(url); + FilesystemImporter.cwd.modificationTime(url); bool couldCanonicalize(Uri url, Uri canonicalUrl) => (url.scheme == 'file' || url.scheme == 'package' || url.scheme == '') && - _filesystemImporter.couldCanonicalize(Uri(path: url.path), canonicalUrl); + FilesystemImporter.cwd + .couldCanonicalize(Uri(path: url.path), canonicalUrl); String toString() => "package:..."; } diff --git a/lib/src/js.dart b/lib/src/js.dart index 9a2c51c06..3246fc743 100644 --- a/lib/src/js.dart +++ b/lib/src/js.dart @@ -5,6 +5,7 @@ import 'js/exception.dart'; import 'js/exports.dart'; import 'js/compile.dart'; +import 'js/compiler.dart'; import 'js/legacy.dart'; import 'js/legacy/types.dart'; import 'js/legacy/value.dart'; @@ -24,6 +25,11 @@ void main() { exports.compileAsync = allowInteropNamed('sass.compileAsync', compileAsync); exports.compileStringAsync = allowInteropNamed('sass.compileStringAsync', compileStringAsync); + exports.initCompiler = allowInteropNamed('sass.initCompiler', initCompiler); + exports.initAsyncCompiler = + allowInteropNamed('sass.initAsyncCompiler', initAsyncCompiler); + exports.Compiler = compilerClass; + exports.AsyncCompiler = asyncCompilerClass; exports.Value = valueClass; exports.SassBoolean = booleanClass; exports.SassArgumentList = argumentListClass; @@ -45,6 +51,7 @@ void main() { silent: JSLogger( warn: allowInteropNamed('sass.Logger.silent.warn', (_, __) {}), debug: allowInteropNamed('sass.Logger.silent.debug', (_, __) {}))); + exports.NodePackageImporter = nodePackageImporterClass; exports.info = "dart-sass\t${const String.fromEnvironment('version')}\t(Sass Compiler)\t" diff --git a/lib/src/js/compile.dart b/lib/src/js/compile.dart index b0a192a9e..a08806975 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -6,6 +6,7 @@ import 'package:cli_pkg/js.dart'; import 'package:node_interop/js.dart'; import 'package:node_interop/util.dart' hide futureToPromise; import 'package:term_glyph/term_glyph.dart' as glyph; +import 'package:path/path.dart' as p; import '../../sass.dart'; import '../importer/no_op.dart'; @@ -13,6 +14,7 @@ import '../importer/js_to_dart/async.dart'; import '../importer/js_to_dart/async_file.dart'; import '../importer/js_to_dart/file.dart'; import '../importer/js_to_dart/sync.dart'; +import '../importer/node_package.dart'; import '../io.dart'; import '../logger/js_to_dart.dart'; import '../util/nullable.dart'; @@ -20,6 +22,7 @@ import 'compile_options.dart'; import 'compile_result.dart'; import 'exception.dart'; import 'importer.dart'; +import 'reflection.dart'; import 'utils.dart'; /// The JS API `compile` function. @@ -182,6 +185,8 @@ OutputStyle _parseOutputStyle(String? style) => switch (style) { /// Converts [importer] into an [AsyncImporter] that can be used with /// [compileAsync] or [compileStringAsync]. AsyncImporter _parseAsyncImporter(Object? importer) { + if (importer is NodePackageImporter) return importer; + if (importer == null) jsThrow(JsError("Importers may not be null.")); importer as JSImporter; @@ -207,6 +212,8 @@ AsyncImporter _parseAsyncImporter(Object? importer) { /// Converts [importer] into a synchronous [Importer]. Importer _parseImporter(Object? importer) { + if (importer is NodePackageImporter) return importer; + if (importer == null) jsThrow(JsError("Importers may not be null.")); importer as JSImporter; @@ -243,7 +250,7 @@ List? _normalizeNonCanonicalSchemes(Object? schemes) => }; /// Implements the simplification algorithm for custom function return `Value`s. -/// {@link https://github.com/sass/sass/blob/main/spec/types/calculation.md#simplifying-a-calculationvalue} +/// See https://github.com/sass/sass/blob/main/spec/types/calculation.md#simplifying-a-calculationvalue Value _simplifyValue(Value value) => switch (value) { SassCalculation() => switch (( // Match against... @@ -321,3 +328,19 @@ List _parseFunctions(Object? functions, {bool asynch = false}) { }); return result; } + +/// The exported `NodePackageImporter` class that can be added to the +/// `importers` option to enable loading `pkg:` URLs from `node_modules`. +final JSClass nodePackageImporterClass = () { + var jsClass = createJSClass( + 'sass.NodePackageImporter', + (Object self, [String? entrypointDirectory]) => NodePackageImporter( + switch ((entrypointDirectory, entrypointFilename)) { + ((var directory?, _)) => directory, + (_, var filename?) => p.dirname(filename), + _ => throw "The Node package importer cannot determine an entry " + "point because `require.main.filename` is not defined. Please " + "provide an `entryPointDirectory` to the `NodePackageImporter`." + })); + return jsClass; +}(); diff --git a/lib/src/js/compiler.dart b/lib/src/js/compiler.dart new file mode 100644 index 000000000..ab1886b3f --- /dev/null +++ b/lib/src/js/compiler.dart @@ -0,0 +1,113 @@ +// Copyright 2024 Google LLC. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:js_util'; + +import 'package:async/async.dart'; +import 'package:node_interop/js.dart'; + +import 'compile.dart'; +import 'compile_options.dart'; +import 'reflection.dart'; +import 'utils.dart'; + +/// The Dart Compiler class. +class Compiler { + /// A flag signifying whether the instance has been disposed. + bool _disposed = false; + + /// Checks if `dispose()` has been called on this instance, and throws an + /// error if it has. Used to verify that compilation methods are not called + /// after disposal. + void _throwIfDisposed() { + if (_disposed) { + jsThrow(JsError('Compiler has already been disposed.')); + } + } +} + +/// The Dart Async Compiler class. +class AsyncCompiler extends Compiler { + /// A set of all compilations, tracked to ensure all compilations complete + /// before async disposal resolves. + final FutureGroup compilations = FutureGroup(); + + /// Adds a compilation to the FutureGroup. + void addCompilation(Promise compilation) { + Future comp = promiseToFuture(compilation); + var wrappedComp = comp.catchError((err) { + /// Ignore errors so FutureGroup doesn't close when a compilation fails. + }); + compilations.add(wrappedComp); + } +} + +/// The JavaScript `Compiler` class. +final JSClass compilerClass = () { + var jsClass = createJSClass( + 'sass.Compiler', + (Object self) => { + jsThrow(JsError(("Compiler can not be directly constructed. " + "Please use `sass.initCompiler()` instead."))) + }); + + jsClass.defineMethods({ + 'compile': (Compiler self, String path, [CompileOptions? options]) { + self._throwIfDisposed(); + return compile(path, options); + }, + 'compileString': (Compiler self, String source, + [CompileStringOptions? options]) { + self._throwIfDisposed(); + return compileString(source, options); + }, + 'dispose': (Compiler self) { + self._disposed = true; + }, + }); + + getJSClass(Compiler()).injectSuperclass(jsClass); + return jsClass; +}(); + +Compiler initCompiler() => Compiler(); + +/// The JavaScript `AsyncCompiler` class. +final JSClass asyncCompilerClass = () { + var jsClass = createJSClass( + 'sass.AsyncCompiler', + (Object self) => { + jsThrow(JsError(("AsyncCompiler can not be directly constructed. " + "Please use `sass.initAsyncCompiler()` instead."))) + }); + + jsClass.defineMethods({ + 'compileAsync': (AsyncCompiler self, String path, + [CompileOptions? options]) { + self._throwIfDisposed(); + var compilation = compileAsync(path, options); + self.addCompilation(compilation); + return compilation; + }, + 'compileStringAsync': (AsyncCompiler self, String source, + [CompileStringOptions? options]) { + self._throwIfDisposed(); + var compilation = compileStringAsync(source, options); + self.addCompilation(compilation); + return compilation; + }, + 'dispose': (AsyncCompiler self) { + self._disposed = true; + return futureToPromise((() async { + self.compilations.close(); + await self.compilations.future; + })()); + } + }); + + getJSClass(AsyncCompiler()).injectSuperclass(jsClass); + return jsClass; +}(); + +Promise initAsyncCompiler() => futureToPromise((() async => AsyncCompiler())()); diff --git a/lib/src/js/exports.dart b/lib/src/js/exports.dart index ee5a74471..9a45268a8 100644 --- a/lib/src/js/exports.dart +++ b/lib/src/js/exports.dart @@ -19,9 +19,14 @@ class Exports { external set compileStringAsync(Function function); external set compile(Function function); external set compileAsync(Function function); + external set initCompiler(Function function); + external set initAsyncCompiler(Function function); + external set Compiler(JSClass function); + external set AsyncCompiler(JSClass function); external set info(String info); external set Exception(JSClass function); external set Logger(LoggerNamespace namespace); + external set NodePackageImporter(JSClass function); // Value APIs external set Value(JSClass function); diff --git a/lib/src/js/legacy.dart b/lib/src/js/legacy.dart index 5c5ad533a..ed4ba7584 100644 --- a/lib/src/js/legacy.dart +++ b/lib/src/js/legacy.dart @@ -10,6 +10,9 @@ import 'dart:typed_data'; import 'package:cli_pkg/js.dart'; import 'package:node_interop/js.dart'; import 'package:path/path.dart' as p; +import '../async_import_cache.dart'; +import '../import_cache.dart'; +import '../importer/node_package.dart'; import '../callable.dart'; import '../compile.dart'; @@ -76,6 +79,7 @@ Future _renderAsync(RenderOptions options) async { if (options.data case var data?) { result = await compileStringAsync(data, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImportersAsync(options, start), functions: _parseFunctions(options, start, asynch: true), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -92,6 +96,7 @@ Future _renderAsync(RenderOptions options) async { } else if (file != null) { result = await compileAsync(file, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImportersAsync(options, start), functions: _parseFunctions(options, start, asynch: true), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -129,6 +134,7 @@ RenderResult renderSync(RenderOptions options) { if (options.data case var data?) { result = compileString(data, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImporters(options, start), functions: _parseFunctions(options, start).cast(), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -145,6 +151,7 @@ RenderResult renderSync(RenderOptions options) { } else if (file != null) { result = compile(file, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImporters(options, start), functions: _parseFunctions(options, start).cast(), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -289,6 +296,23 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) { return NodeImporter(contextOptions, includePaths, importers); } +/// Creates an [AsyncImportCache] for Package Importers. +AsyncImportCache? _parsePackageImportersAsync( + RenderOptions options, DateTime start) { + if (options.pkgImporter is NodePackageImporter) { + return AsyncImportCache.only([options.pkgImporter!]); + } + return null; +} + +/// Creates an [ImportCache] for Package Importers. +ImportCache? _parsePackageImporters(RenderOptions options, DateTime start) { + if (options.pkgImporter is NodePackageImporter) { + return ImportCache.only([options.pkgImporter!]); + } + return null; +} + /// Creates the [RenderContextOptions] for the `this` context in which custom /// functions and importers will be evaluated. RenderContextOptions _contextOptions(RenderOptions options, DateTime start) { diff --git a/lib/src/js/legacy/render_options.dart b/lib/src/js/legacy/render_options.dart index 3357166de..ac8cc61b8 100644 --- a/lib/src/js/legacy/render_options.dart +++ b/lib/src/js/legacy/render_options.dart @@ -4,6 +4,7 @@ import 'package:js/js.dart'; +import '../../importer/node_package.dart'; import '../logger.dart'; import 'fiber.dart'; @@ -13,6 +14,7 @@ class RenderOptions { external String? get file; external String? get data; external Object? get importer; + external NodePackageImporter? get pkgImporter; external Object? get functions; external List? get includePaths; external bool? get indentedSyntax; @@ -36,6 +38,7 @@ class RenderOptions { {String? file, String? data, Object? importer, + NodePackageImporter? pkgImporter, Object? functions, List? includePaths, bool? indentedSyntax, diff --git a/lib/src/js/module.dart b/lib/src/js/module.dart new file mode 100644 index 000000000..0723b994e --- /dev/null +++ b/lib/src/js/module.dart @@ -0,0 +1,26 @@ +// Copyright 2024 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:js/js.dart'; + +@JS('nodeModule') +external JSModule get module; + +/// A Dart API for the [`node:module`] module. +/// +/// [`node:module`]: https://nodejs.org/api/module.html#modules-nodemodule-api +@JS() +@anonymous +class JSModule { + /// See https://nodejs.org/api/module.html#modulecreaterequirefilename. + external JSModuleRequire createRequire(String filename); +} + +/// A `require` function returned by `module.createRequire()`. +@JS() +@anonymous +class JSModuleRequire { + /// See https://nodejs.org/api/modules.html#requireresolverequest-options. + external String resolve(String filename); +} diff --git a/lib/src/js/source_span.dart b/lib/src/js/source_span.dart index 998db03ad..057aec500 100644 --- a/lib/src/js/source_span.dart +++ b/lib/src/js/source_span.dart @@ -4,6 +4,7 @@ import 'package:source_span/source_span.dart'; +import '../util/lazy_file_span.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; import 'reflection.dart'; @@ -14,8 +15,9 @@ import 'utils.dart'; void updateSourceSpanPrototype() { var span = SourceFile.fromString('').span(0); var multiSpan = MultiSpan(span, '', {}); + var lazySpan = LazyFileSpan(() => span); - for (var item in [span, multiSpan]) { + for (var item in [span, multiSpan, lazySpan]) { getJSClass(item).defineGetters({ 'start': (FileSpan span) => span.start, 'end': (FileSpan span) => span.end, diff --git a/lib/src/js/utils.dart b/lib/src/js/utils.dart index 349a7b7bb..e291054b5 100644 --- a/lib/src/js/utils.dart +++ b/lib/src/js/utils.dart @@ -5,7 +5,7 @@ import 'dart:js_util'; import 'dart:typed_data'; -import 'package:node_interop/js.dart'; +import 'package:node_interop/node.dart' hide module; import 'package:js/js.dart'; import 'package:js/js_util.dart'; @@ -14,6 +14,7 @@ import '../utils.dart'; import '../value.dart'; import 'array.dart'; import 'function.dart'; +import 'module.dart'; import 'reflection.dart'; import 'url.dart'; @@ -238,3 +239,23 @@ Syntax parseSyntax(String? syntax) => switch (syntax) { 'css' => Syntax.css, _ => jsThrow(JsError('Unknown syntax "$syntax".')) }; + +/// The path to the Node.js entrypoint, if one can be located. +String? get entrypointFilename { + if (_requireMain?.filename case var filename?) { + return filename; + } else if (process.argv case [_, String path, ...]) { + return module.createRequire(path).resolve(path); + } else { + return null; + } +} + +@JS("require.main") +external _RequireMain? get _requireMain; + +@JS() +@anonymous +class _RequireMain { + external String? get filename; +} diff --git a/lib/src/parse/at_root_query.dart b/lib/src/parse/at_root_query.dart index 11eee11f2..a107458c1 100644 --- a/lib/src/parse/at_root_query.dart +++ b/lib/src/parse/at_root_query.dart @@ -5,16 +5,12 @@ import 'package:charcode/charcode.dart'; import '../ast/sass.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import 'parser.dart'; /// A parser for `@at-root` queries. class AtRootQueryParser extends Parser { - AtRootQueryParser(String contents, - {Object? url, Logger? logger, InterpolationMap? interpolationMap}) - : super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + AtRootQueryParser(super.contents, + {super.url, super.logger, super.interpolationMap}); AtRootQuery parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index 6ed9123b7..747d22c49 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -7,7 +7,6 @@ import 'package:string_scanner/string_scanner.dart'; import '../ast/sass.dart'; import '../functions.dart'; -import '../logger.dart'; import 'scss.dart'; /// The set of all function names disallowed in plain CSS. @@ -31,10 +30,11 @@ final _disallowedFunctionNames = class CssParser extends ScssParser { bool get plainCss => true; - CssParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + CssParser(super.contents, {super.url, super.logger}); + + bool silentComment() { + if (inExpression) return false; - void silentComment() { var start = scanner.state; super.silentComment(); error("Silent comments aren't allowed in plain CSS.", diff --git a/lib/src/parse/keyframe_selector.dart b/lib/src/parse/keyframe_selector.dart index 71908c3e3..25a690813 100644 --- a/lib/src/parse/keyframe_selector.dart +++ b/lib/src/parse/keyframe_selector.dart @@ -4,17 +4,13 @@ import 'package:charcode/charcode.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import '../util/character.dart'; import 'parser.dart'; /// A parser for `@keyframes` block selectors. class KeyframeSelectorParser extends Parser { - KeyframeSelectorParser(String contents, - {Object? url, Logger? logger, InterpolationMap? interpolationMap}) - : super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + KeyframeSelectorParser(super.contents, + {super.url, super.logger, super.interpolationMap}); List parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/media_query.dart b/lib/src/parse/media_query.dart index be86a1994..ce54dae57 100644 --- a/lib/src/parse/media_query.dart +++ b/lib/src/parse/media_query.dart @@ -5,17 +5,13 @@ import 'package:charcode/charcode.dart'; import '../ast/css.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import '../utils.dart'; import 'parser.dart'; /// A parser for `@media` queries. class MediaQueryParser extends Parser { - MediaQueryParser(String contents, - {Object? url, Logger? logger, InterpolationMap? interpolationMap}) - : super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + MediaQueryParser(super.contents, + {super.url, super.logger, super.interpolationMap}); List parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index 6dd17c80d..a16b871e7 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -114,8 +114,7 @@ class Parser { switch (scanner.peekChar(1)) { case $slash: - silentComment(); - return true; + return silentComment(); case $asterisk: loudComment(); return true; @@ -135,12 +134,15 @@ class Parser { } /// Consumes and ignores a silent (Sass-style) comment. + /// + /// Returns whether the comment was consumed. @protected - void silentComment() { + bool silentComment() { scanner.expect("//"); while (!scanner.isDone && !scanner.peekChar().isNewline) { scanner.readChar(); } + return true; } /// Consumes and ignores a loud (CSS-style) comment. diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index 95fd054c5..78dea3179 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -7,7 +7,6 @@ import 'package:string_scanner/string_scanner.dart'; import '../ast/sass.dart'; import '../interpolation_buffer.dart'; -import '../logger.dart'; import '../util/character.dart'; import '../value.dart'; import 'stylesheet.dart'; @@ -38,8 +37,7 @@ class SassParser extends StylesheetParser { bool get indented => true; - SassParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + SassParser(super.contents, {super.url, super.logger}); Interpolation styleRuleSelector() { var start = scanner.state; diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index cc432e9c8..67b5c0f4b 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -16,8 +16,7 @@ class ScssParser extends StylesheetParser { bool get indented => false; int get currentIndentation => 0; - ScssParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + ScssParser(super.contents, {super.url, super.logger}); Interpolation styleRuleSelector() => almostAnyValue(); diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 75df8b205..97a334166 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -6,8 +6,6 @@ import 'package:charcode/charcode.dart'; import '../ast/css/value.dart'; import '../ast/selector.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import '../util/character.dart'; import '../utils.dart'; import 'parser.dart'; @@ -33,19 +31,24 @@ class SelectorParser extends Parser { /// Whether this parser allows the parent selector `&`. final bool _allowParent; - /// Whether this parser allows placeholder selectors beginning with `%`. - final bool _allowPlaceholder; + /// Whether to parse the selector as plain CSS. + final bool _plainCss; - SelectorParser(String contents, - {Object? url, - Logger? logger, - InterpolationMap? interpolationMap, + /// Creates a parser that parses CSS selectors. + /// + /// If [allowParent] is `false`, this will throw a [SassFormatException] if + /// the selector includes the parent selector `&`. + /// + /// If [plainCss] is `true`, this will parse the selector as a plain CSS + /// selector rather than a Sass selector. + SelectorParser(super.contents, + {super.url, + super.logger, + super.interpolationMap, bool allowParent = true, - bool allowPlaceholder = true}) + bool plainCss = false}) : _allowParent = allowParent, - _allowPlaceholder = allowPlaceholder, - super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + _plainCss = plainCss; SelectorList parse() { return wrapSpanFormatException(() { @@ -169,7 +172,9 @@ class SelectorParser extends Parser { } } - if (lastCompound != null) { + if (combinators.isNotEmpty && _plainCss) { + scanner.error("expected selector."); + } else if (lastCompound != null) { components.add(ComplexSelectorComponent( lastCompound, combinators, spanFrom(componentStart))); } else if (combinators.isNotEmpty) { @@ -188,8 +193,8 @@ class SelectorParser extends Parser { var start = scanner.state; var components = [_simpleSelector()]; - while (isSimpleSelectorStart(scanner.peekChar())) { - components.add(_simpleSelector(allowParent: false)); + while (_isSimpleSelectorStart(scanner.peekChar())) { + components.add(_simpleSelector(allowParent: _plainCss)); } return CompoundSelector(components, spanFrom(start)); @@ -211,8 +216,8 @@ class SelectorParser extends Parser { return _idSelector(); case $percent: var selector = _placeholderSelector(); - if (!_allowPlaceholder) { - error("Placeholder selectors aren't allowed here.", + if (_plainCss) { + error("Placeholder selectors aren't allowed in plain CSS.", scanner.spanFrom(start)); } return selector; @@ -344,6 +349,11 @@ class SelectorParser extends Parser { var start = scanner.state; scanner.expectChar($ampersand); var suffix = lookingAtIdentifierBody() ? identifierBody() : null; + if (_plainCss && suffix != null) { + scanner.error("Parent selectors can't have suffixes in plain CSS.", + position: start.position, length: scanner.position - start.position); + } + return ParentSelector(spanFrom(start), suffix: suffix); } @@ -461,4 +471,12 @@ class SelectorParser extends Parser { spanFrom(start)); } } + + // Returns whether [character] can start a simple selector in the middle of a + // compound selector. + bool _isSimpleSelectorStart(int? character) => switch (character) { + $asterisk || $lbracket || $dot || $hash || $percent || $colon => true, + $ampersand => _plainCss, + _ => false + }; } diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index b5da5ff48..6393ea57d 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -56,6 +56,11 @@ abstract class StylesheetParser extends Parser { /// Whether the parser is currently within a parenthesized expression. var _inParentheses = false; + /// Whether the parser is currently within an expression. + @protected + bool get inExpression => _inExpression; + var _inExpression = false; + /// A map from all variable names that are assigned with `!global` in the /// current stylesheet to the nodes where they're defined. /// @@ -69,8 +74,7 @@ abstract class StylesheetParser extends Parser { @protected SilentComment? lastSilentComment; - StylesheetParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + StylesheetParser(super.contents, {super.url, super.logger}); // ## Statements @@ -325,10 +329,6 @@ abstract class StylesheetParser extends Parser { /// parsed as a selector and never as a property with nested properties /// beneath it. Statement _declarationOrStyleRule() { - if (plainCss && _inStyleRule && !_inUnknownAtRule) { - return _propertyOrVariableDeclaration(); - } - // The indented syntax allows a single backslash to distinguish a style rule // from old-style property syntax. We don't support old property syntax, but // we do support the backslash because it's easy to do. @@ -401,10 +401,7 @@ abstract class StylesheetParser extends Parser { } var postColonWhitespace = rawText(whitespace); - if (lookingAtChildren()) { - return _withChildren(_declarationChild, start, - (children, span) => Declaration.nested(name, children, span)); - } + if (_tryDeclarationChildren(name, start) case var nested?) return nested; midBuffer.write(postColonWhitespace); var couldBeSelector = @@ -440,12 +437,8 @@ abstract class StylesheetParser extends Parser { return nameBuffer; } - if (lookingAtChildren()) { - return _withChildren( - _declarationChild, - start, - (children, span) => - Declaration.nested(name, children, span, value: value)); + if (_tryDeclarationChildren(name, start, value: value) case var nested?) { + return nested; } else { expectStatementSeparator(); return Declaration(name, value, scanner.spanFrom(start)); @@ -550,31 +543,36 @@ abstract class StylesheetParser extends Parser { } whitespace(); - - if (lookingAtChildren()) { - if (plainCss) { - scanner.error("Nested declarations aren't allowed in plain CSS."); - } - return _withChildren(_declarationChild, start, - (children, span) => Declaration.nested(name, children, span)); - } + if (_tryDeclarationChildren(name, start) case var nested?) return nested; var value = _expression(); - if (lookingAtChildren()) { - if (plainCss) { - scanner.error("Nested declarations aren't allowed in plain CSS."); - } - return _withChildren( - _declarationChild, - start, - (children, span) => - Declaration.nested(name, children, span, value: value)); + if (_tryDeclarationChildren(name, start, value: value) case var nested?) { + return nested; } else { expectStatementSeparator(); return Declaration(name, value, scanner.spanFrom(start)); } } + /// Tries parsing nested children of a declaration whose [name] has already + /// been parsed, and returns `null` if it doesn't have any. + /// + /// If [value] is passed, it's used as the value of the peroperty without + /// nesting. + Declaration? _tryDeclarationChildren( + Interpolation name, LineScannerState start, + {Expression? value}) { + if (!lookingAtChildren()) return null; + if (plainCss) { + scanner.error("Nested declarations aren't allowed in plain CSS."); + } + return _withChildren( + _declarationChild, + start, + (children, span) => + Declaration.nested(name, children, span, value: value)); + } + /// Consumes a statement that's allowed within a declaration. Statement _declarationChild() => scanner.peekChar() == $at ? _declarationAtRule() @@ -1692,7 +1690,9 @@ abstract class StylesheetParser extends Parser { } var start = scanner.state; + var wasInExpression = _inExpression; var wasInParentheses = _inParentheses; + _inExpression = true; // We use the convention below of referring to nullable variables that are // shared across anonymous functions in this method with a trailing @@ -2045,11 +2045,13 @@ abstract class StylesheetParser extends Parser { _inParentheses = wasInParentheses; var singleExpression = singleExpression_; if (singleExpression != null) commaExpressions.add(singleExpression); + _inExpression = wasInExpression; return ListExpression(commaExpressions, ListSeparator.comma, scanner.spanFrom(beforeBracket ?? start), brackets: bracketList); } else if (bracketList && spaceExpressions != null) { resolveOperations(); + _inExpression = wasInExpression; return ListExpression(spaceExpressions..add(singleExpression_!), ListSeparator.space, scanner.spanFrom(beforeBracket!), brackets: true); @@ -2060,6 +2062,7 @@ abstract class StylesheetParser extends Parser { ListSeparator.undecided, scanner.spanFrom(beforeBracket!), brackets: true); } + _inExpression = wasInExpression; return singleExpression_!; } } diff --git a/lib/src/util/box.dart b/lib/src/util/box.dart index cfd076669..50a9eb750 100644 --- a/lib/src/util/box.dart +++ b/lib/src/util/box.dart @@ -13,7 +13,7 @@ class Box { Box._(this._inner); - bool operator ==(Object? other) => other is Box && other._inner == _inner; + bool operator ==(Object other) => other is Box && other._inner == _inner; int get hashCode => _inner.hashCode; } diff --git a/lib/src/util/character.dart b/lib/src/util/character.dart index ea4085d29..7141be67a 100644 --- a/lib/src/util/character.dart +++ b/lib/src/util/character.dart @@ -92,16 +92,6 @@ int combineSurrogates(int highSurrogate, int lowSurrogate) => // high/low surrogates. 0x10000 + ((highSurrogate & 0x3FF) << 10) + (lowSurrogate & 0x3FF); -// Returns whether [character] can start a simple selector other than a type -// selector. -bool isSimpleSelectorStart(int? character) => - character == $asterisk || - character == $lbracket || - character == $dot || - character == $hash || - character == $percent || - character == $colon; - /// Returns whether [identifier] is module-private. /// /// Assumes [identifier] is a valid Sass identifier. diff --git a/lib/src/value/argument_list.dart b/lib/src/value/argument_list.dart index f9b4b5014..23de2db7a 100644 --- a/lib/src/value/argument_list.dart +++ b/lib/src/value/argument_list.dart @@ -42,8 +42,6 @@ class SassArgumentList extends SassList { bool get wereKeywordsAccessed => _wereKeywordsAccessed; var _wereKeywordsAccessed = false; - SassArgumentList(Iterable contents, Map keywords, - ListSeparator separator) - : _keywords = Map.unmodifiable(keywords), - super(contents, separator); + SassArgumentList(super.contents, Map keywords, super.separator) + : _keywords = Map.unmodifiable(keywords); } diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index 7272b7c59..a81d1d9a9 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -19,8 +19,7 @@ class UnitlessSassNumber extends SassNumber { bool get hasUnits => false; bool get hasComplexUnits => false; - UnitlessSassNumber(double value, [(SassNumber, SassNumber)? asSlash]) - : super.protected(value, asSlash); + UnitlessSassNumber(super.value, [super.asSlash]) : super.protected(); SassNumber withValue(num value) => UnitlessSassNumber(value.toDouble()); diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 8c97297c5..b48ffc27a 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -338,9 +338,10 @@ final class _EvaluateVisitor Logger? logger, bool quietDeps = false, bool sourceMap = false}) - : _importCache = nodeImporter == null - ? importCache ?? AsyncImportCache.none(logger: logger) - : null, + : _importCache = importCache ?? + (nodeImporter == null + ? AsyncImportCache.none(logger: logger) + : null), _nodeImporter = nodeImporter, _logger = logger ?? const Logger.stderr(), _quietDeps = quietDeps, @@ -546,7 +547,8 @@ final class _EvaluateVisitor namesInErrors: true); _assertConfigurationIsEmpty(configuration, nameInError: true); }, url: "sass:meta"), - BuiltInCallable.mixin("apply", r"$mixin, $args...", (arguments) async { + AsyncBuiltInCallable.mixin("apply", r"$mixin, $args...", + (arguments) async { var mixin = arguments[0]; var args = arguments[1] as SassArgumentList; @@ -1707,7 +1709,9 @@ final class _EvaluateVisitor return (stylesheet, importer: importer, isDependency: isDependency); } } - } else { + } + + if (_nodeImporter != null) { if (await _importLikeNode( url, baseUrl ?? _stylesheet.span.sourceUrl, forImport) case var result?) { @@ -2009,16 +2013,32 @@ final class _EvaluateVisitor } var parsedSelector = SelectorList.parse(selectorText, - interpolationMap: selectorMap, - allowParent: !_stylesheet.plainCss, - allowPlaceholder: !_stylesheet.plainCss, - logger: _logger) - .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + interpolationMap: selectorMap, + plainCss: _stylesheet.plainCss, + logger: _logger); + + var nest = !(_styleRule?.fromPlainCss ?? false); + if (nest) { + if (_stylesheet.plainCss) { + for (var complex in parsedSelector.components) { + if (complex.leadingCombinators case [var first, ...] + when _stylesheet.plainCss) { + throw _exception( + "Top-level leading combinators aren't allowed in plain CSS.", + first.span); + } + } + } + + parsedSelector = parsedSelector.nestWithin( + _styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: _stylesheet.plainCss); + } var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: parsedSelector); + originalSelector: parsedSelector, fromPlainCss: _stylesheet.plainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; await _withParent(rule, () async { @@ -2028,7 +2048,7 @@ final class _EvaluateVisitor } }); }, - through: (node) => node is CssStyleRule, + through: nest ? (node) => node is CssStyleRule : null, scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; @@ -2046,13 +2066,15 @@ final class _EvaluateVisitor complex.span.trimRight(), Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { - _warn( - 'The selector "${complex.toString().trim()}" is invalid CSS.\n' - 'This will be an error in Dart Sass 2.0.0.\n' - '\n' - 'More info: https://sass-lang.com/d/bogus-combinators', - complex.span.trimRight(), - Deprecation.bogusCombinators); + if (!_stylesheet.plainCss) { + _warn( + 'The selector "${complex.toString().trim()}" is invalid CSS.\n' + 'This will be an error in Dart Sass 2.0.0.\n' + '\n' + 'More info: https://sass-lang.com/d/bogus-combinators', + complex.span.trimRight(), + Deprecation.bogusCombinators); + } } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -3384,12 +3406,15 @@ final class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.resolveParentSelectors( - styleRule?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + var nest = !(_styleRule?.fromPlainCss ?? false); + var originalSelector = nest + ? node.selector.nestWithin(styleRule?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: node.fromPlainCss) + : node.selector; var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: originalSelector); + originalSelector: originalSelector, fromPlainCss: node.fromPlainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; await _withParent(rule, () async { @@ -3398,7 +3423,7 @@ final class _EvaluateVisitor await child.accept(this); } }); - }, through: (node) => node is CssStyleRule, scopeWhen: false); + }, through: nest ? (node) => node is CssStyleRule : null, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; if (_parent.children case [..., var lastChild] when styleRule == null) { diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 93f2a4540..35dbfbc59 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: e65c7f962e10c657d2292c46474291ad217dea9f +// Checksum: e720118b3b7ee5519f2ff55582818eeee224cfec // // ignore_for_file: unused_import @@ -346,9 +346,8 @@ final class _EvaluateVisitor Logger? logger, bool quietDeps = false, bool sourceMap = false}) - : _importCache = nodeImporter == null - ? importCache ?? ImportCache.none(logger: logger) - : null, + : _importCache = importCache ?? + (nodeImporter == null ? ImportCache.none(logger: logger) : null), _nodeImporter = nodeImporter, _logger = logger ?? const Logger.stderr(), _quietDeps = quietDeps, @@ -1704,7 +1703,9 @@ final class _EvaluateVisitor return (stylesheet, importer: importer, isDependency: isDependency); } } - } else { + } + + if (_nodeImporter != null) { if (_importLikeNode( url, baseUrl ?? _stylesheet.span.sourceUrl, forImport) case var result?) { @@ -2002,16 +2003,32 @@ final class _EvaluateVisitor } var parsedSelector = SelectorList.parse(selectorText, - interpolationMap: selectorMap, - allowParent: !_stylesheet.plainCss, - allowPlaceholder: !_stylesheet.plainCss, - logger: _logger) - .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + interpolationMap: selectorMap, + plainCss: _stylesheet.plainCss, + logger: _logger); + + var nest = !(_styleRule?.fromPlainCss ?? false); + if (nest) { + if (_stylesheet.plainCss) { + for (var complex in parsedSelector.components) { + if (complex.leadingCombinators case [var first, ...] + when _stylesheet.plainCss) { + throw _exception( + "Top-level leading combinators aren't allowed in plain CSS.", + first.span); + } + } + } + + parsedSelector = parsedSelector.nestWithin( + _styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: _stylesheet.plainCss); + } var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: parsedSelector); + originalSelector: parsedSelector, fromPlainCss: _stylesheet.plainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; _withParent(rule, () { @@ -2021,7 +2038,7 @@ final class _EvaluateVisitor } }); }, - through: (node) => node is CssStyleRule, + through: nest ? (node) => node is CssStyleRule : null, scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; @@ -2039,13 +2056,15 @@ final class _EvaluateVisitor complex.span.trimRight(), Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { - _warn( - 'The selector "${complex.toString().trim()}" is invalid CSS.\n' - 'This will be an error in Dart Sass 2.0.0.\n' - '\n' - 'More info: https://sass-lang.com/d/bogus-combinators', - complex.span.trimRight(), - Deprecation.bogusCombinators); + if (!_stylesheet.plainCss) { + _warn( + 'The selector "${complex.toString().trim()}" is invalid CSS.\n' + 'This will be an error in Dart Sass 2.0.0.\n' + '\n' + 'More info: https://sass-lang.com/d/bogus-combinators', + complex.span.trimRight(), + Deprecation.bogusCombinators); + } } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -3355,12 +3374,15 @@ final class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.resolveParentSelectors( - styleRule?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + var nest = !(_styleRule?.fromPlainCss ?? false); + var originalSelector = nest + ? node.selector.nestWithin(styleRule?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: node.fromPlainCss) + : node.selector; var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: originalSelector); + originalSelector: originalSelector, fromPlainCss: node.fromPlainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; _withParent(rule, () { @@ -3369,7 +3391,7 @@ final class _EvaluateVisitor child.accept(this); } }); - }, through: (node) => node is CssStyleRule, scopeWhen: false); + }, through: nest ? (node) => node is CssStyleRule : null, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; if (_parent.children case [..., var lastChild] when styleRule == null) { diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 62527112f..eb9a65e4b 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -6,7 +6,6 @@ import 'dart:math' as math; import 'dart:typed_data'; import 'package:charcode/charcode.dart'; -import 'package:collection/collection.dart'; import 'package:source_maps/source_maps.dart'; import 'package:string_scanner/string_scanner.dart'; @@ -489,13 +488,8 @@ final class _SerializeVisitor void _writeCalculationValue(Object value) { switch (value) { - case SassNumber(value: double(isFinite: false), hasComplexUnits: true): - if (!_inspect) { - throw SassScriptException("$value isn't a valid CSS value."); - } - - _writeNumber(value.value); - _buffer.write(value.unitString); + case SassNumber(hasComplexUnits: true) when !_inspect: + throw SassScriptException("$value isn't a valid CSS value."); case SassNumber(value: double(isFinite: false)): switch (value.value) { @@ -507,12 +501,15 @@ final class _SerializeVisitor _buffer.write('NaN'); } - if (value.numeratorUnits.firstOrNull case var unit?) { - _writeOptionalSpace(); - _buffer.writeCharCode($asterisk); - _writeOptionalSpace(); - _buffer.writeCharCode($1); - _buffer.write(unit); + _writeCalculationUnits(value.numeratorUnits, value.denominatorUnits); + + case SassNumber(hasComplexUnits: true): + _writeNumber(value.value); + if (value.numeratorUnits case [var first, ...var rest]) { + _buffer.write(first); + _writeCalculationUnits(rest, value.denominatorUnits); + } else { + _writeCalculationUnits([], value.denominatorUnits); } case Value(): @@ -534,14 +531,36 @@ final class _SerializeVisitor _parenthesizeCalculationRhs(operator, right.operator)) || (operator == CalculationOperator.dividedBy && right is SassNumber && - !right.value.isFinite && - right.hasUnits); + (right.value.isFinite + ? right.hasComplexUnits + : right.hasUnits)); if (parenthesizeRight) _buffer.writeCharCode($lparen); _writeCalculationValue(right); if (parenthesizeRight) _buffer.writeCharCode($rparen); } } + /// Writes the complex numerator and denominator units beyond the first + /// numerator unit for a number as they appear in a calculation. + void _writeCalculationUnits( + List numeratorUnits, List denominatorUnits) { + for (var unit in numeratorUnits) { + _writeOptionalSpace(); + _buffer.writeCharCode($asterisk); + _writeOptionalSpace(); + _buffer.writeCharCode($1); + _buffer.write(unit); + } + + for (var unit in denominatorUnits) { + _writeOptionalSpace(); + _buffer.writeCharCode($slash); + _writeOptionalSpace(); + _buffer.writeCharCode($1); + _buffer.write(unit); + } + } + /// Returns whether the right-hand operation of a calculation should be /// parenthesized. /// @@ -970,16 +989,15 @@ final class _SerializeVisitor return; } - _writeNumber(value.value); - - if (!_inspect) { - if (value.hasComplexUnits) { + if (value.hasComplexUnits) { + if (!_inspect) { throw SassScriptException("$value isn't a valid CSS value."); } - if (value.numeratorUnits case [var first]) _buffer.write(first); + visitCalculation(SassCalculation.unsimplified('calc', [value])); } else { - _buffer.write(value.unitString); + _writeNumber(value.value); + if (value.numeratorUnits case [var first]) _buffer.write(first); } } @@ -1267,7 +1285,8 @@ final class _SerializeVisitor $fs || $gs || $rs || - $us: + $us || + $del: _writeEscape(buffer, char, string, i); case $backslash: diff --git a/package.json b/package.json index 531856ade..823058eef 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,5 @@ "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", "intercept-stdout": "^0.1.2" - }, - "dependencies": { - "sass": "^1.63.5" } } diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index f03ef4d8f..56e40ba63 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,4 +1,4 @@ -## 10.0.0 +## 11.0.0 * **Breaking change:** Remove the `SassApiColor.hasCalculatedRgb` and `.hasCalculatedHsl` extension methods. These can now be determined by checking @@ -49,6 +49,42 @@ * Added `SassNumber.convertValueToUnit()` as a shorthand for `SassNumber.convertValue()` with a single numerator. +## 10.0.0 + +* Remove the `allowPlaceholders` argument from `SelectorList.parse()`. Instead, + it now has a more generic `plainCss` argument which tells it to parse the + selector in plain CSS mode. + +* Rename `SelectorList.resolveParentSelectors` to `SelectorList.nestWithin`. + +## 9.5.0 + +* No user-visible changes. + +## 9.4.2 + +* No user-visible changes. + +## 9.4.1 + +* No user-visible changes. + +## 9.4.0 + +* No user-visible changes. + +## 9.3.0 + +* No user-visible changes. + +## 9.2.7 + +* No user-visible changes. + +## 9.2.6 + +* No user-visible changes. + ## 9.2.5 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 9c08a6fd0..972ffa7f7 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 10.0.0-dev +version: 11.0.0-dev description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - sass: 1.70.0 + sass: 1.74.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index c3341cbf9..bc772286e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.70.0-dev # TODO: update the color-functions deprecation when this is updated +version: 1.74.0-dev # TODO: update the color-functions deprecation when this is updated description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass @@ -14,7 +14,7 @@ dependencies: args: ^2.0.0 async: ^2.5.0 charcode: ^1.2.0 - cli_pkg: ^2.7.0 + cli_pkg: ^2.8.0 cli_repl: ^0.2.1 collection: ^1.16.0 http: "^1.1.0" @@ -42,7 +42,7 @@ dev_dependencies: archive: ^3.1.2 crypto: ^3.0.0 dart_style: ^2.0.0 - dartdoc: ">=6.0.0 <8.0.0" + dartdoc: ">=6.0.0 <9.0.0" grinder: ^0.9.0 node_preamble: ^2.0.2 lints: ">=2.0.0 <4.0.0" diff --git a/test/cli/shared/update.dart b/test/cli/shared/update.dart index 4fc7bfbc1..73b8a21df 100644 --- a/test/cli/shared/update.dart +++ b/test/cli/shared/update.dart @@ -2,6 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test_process/test_process.dart'; @@ -148,6 +149,18 @@ void sharedTests(Future runSass(Iterable arguments)) { await d.file("out.css", "x {y: z}").validate(); }); + // Regression test for #2203 + test("whose sources weren't modified with an absolute path", () async { + await d.file("test.scss", "a {b: c}").create(); + await d.file("out.css", "x {y: z}").create(); + + var sass = await update(["${p.absolute(d.path('test.scss'))}:out.css"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(0); + + await d.file("out.css", "x {y: z}").validate(); + }); + test("whose sibling was modified", () async { await d.file("test1.scss", "a {b: c}").create(); await d.file("out1.css", "x {y: z}").create(); diff --git a/test/dart_api/logger_test.dart b/test/dart_api/logger_test.dart index a1c0ec93f..979b2ba11 100644 --- a/test/dart_api/logger_test.dart +++ b/test/dart_api/logger_test.dart @@ -227,10 +227,6 @@ void main() { mustBeCalled(); })); }); - - test("throws an error outside a callback", () { - expect(() => warn("heck"), throwsStateError); - }); }); } diff --git a/test/embedded/function_test.dart b/test/embedded/function_test.dart index db8917ff3..158d51aa5 100644 --- a/test/embedded/function_test.dart +++ b/test/embedded/function_test.dart @@ -912,7 +912,7 @@ void main() { ..value = 1 ..numerators.addAll(["em", "px", "foo"])), inspect: true), - "1em*px*foo"); + "calc(1em * 1px * 1foo)"); }); test("with one denominator", () async { @@ -923,7 +923,7 @@ void main() { ..value = 1 ..denominators.add("em")), inspect: true), - "1em^-1"); + "calc(1 / 1em)"); }); test("with multiple denominators", () async { @@ -934,7 +934,7 @@ void main() { ..value = 1 ..denominators.addAll(["em", "px", "foo"])), inspect: true), - "1(em*px*foo)^-1"); + "calc(1 / 1em / 1px / 1foo)"); }); test("with numerators and denominators", () async { @@ -946,7 +946,7 @@ void main() { ..numerators.addAll(["em", "px"]) ..denominators.addAll(["s", "foo"])), inspect: true), - "1em*px/s*foo"); + "calc(1em * 1px / 1s / 1foo)"); }); }); diff --git a/test/embedded/protocol_test.dart b/test/embedded/protocol_test.dart index 9e293365e..b77d376e0 100644 --- a/test/embedded/protocol_test.dart +++ b/test/embedded/protocol_test.dart @@ -96,7 +96,7 @@ void main() { Version.parse(response.protocolVersion); // shouldn't throw Version.parse(response.compilerVersion); // shouldn't throw Version.parse(response.implementationVersion); // shouldn't throw - expect(response.implementationName, equals("Dart Sass")); + expect(response.implementationName, equals("dart-sass")); await process.close(); }); diff --git a/test/output_test.dart b/test/output_test.dart index 2023ae65b..c9dcfca1a 100644 --- a/test/output_test.dart +++ b/test/output_test.dart @@ -113,10 +113,55 @@ void main() { }); }); - // Tests for sass/dart-sass#417. + // Tests for sass/dart-sass#2070. // - // Note there's no need for "in Sass" cases as it's not possible to have - // trailing loud comments in the Sass syntax. + // These aren't covered by sass-spec because the inspect format for + // non-literal values isn't covered by the spec. + group("uses a nice format to inspect numbers with complex units", () { + group("finite", () { + test("top-level", () { + expect(compileString(""" + @use 'sass:meta'; + a {b: meta.inspect(1px * 1em)}; + """), equalsIgnoringWhitespace('a { b: calc(1px * 1em); }')); + }); + + test("in calc", () { + expect(compileString(""" + @use 'sass:meta'; + a {b: meta.inspect(calc(1px * 1em))}; + """), equalsIgnoringWhitespace('a { b: calc(1px * 1em); }')); + }); + + test("nested in calc", () { + expect(compileString(""" + @use 'sass:meta'; + a {b: meta.inspect(calc(c / (1px * 1em)))}; + """), equalsIgnoringWhitespace('a { b: calc(c / (1px * 1em)); }')); + }); + + test("numerator and denominator", () { + expect(compileString(""" + @use 'sass:math'; + @use 'sass:meta'; + a {b: meta.inspect(1px * math.div(math.div(1em, 1s), 1x))}; + """), equalsIgnoringWhitespace('a { b: calc(1px * 1em / 1s / 1x); }')); + }); + + test("denominator only", () { + expect(compileString(""" + @use 'sass:math'; + @use 'sass:meta'; + a {b: meta.inspect(math.div(math.div(1, 1s), 1x))}; + """), equalsIgnoringWhitespace('a { b: calc(1 / 1s / 1x); }')); + }); + }); + }); + + // Tests for sass/dart-sass#417. + // + // Note there's no need for "in Sass" cases as it's not possible to have + // trailing loud comments in the Sass syntax. group("preserve trailing loud comments in SCSS", () { test("after open block", () { expect(compileString(""" diff --git a/tool/grind.dart b/tool/grind.dart index 9d320742c..2bf6bf163 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -29,11 +29,14 @@ void main(List args) { pkg.chocolateyNuspec.value = _nuspec; pkg.homebrewRepo.value = "sass/homebrew-sass"; pkg.homebrewFormula.value = "Formula/sass.rb"; + pkg.homebrewEditFormula.value = _updateHomebrewLanguageRevision; pkg.jsRequires.value = [ pkg.JSRequire("immutable", target: pkg.JSRequireTarget.all), pkg.JSRequire("chokidar", target: pkg.JSRequireTarget.cli), pkg.JSRequire("readline", target: pkg.JSRequireTarget.cli), pkg.JSRequire("fs", target: pkg.JSRequireTarget.node), + pkg.JSRequire("module", + target: pkg.JSRequireTarget.node, identifier: 'nodeModule'), pkg.JSRequire("stream", target: pkg.JSRequireTarget.node), pkg.JSRequire("util", target: pkg.JSRequireTarget.node), ]; @@ -51,6 +54,10 @@ void main(List args) { 'compileAsync', 'compileString', 'compileStringAsync', + 'initCompiler', + 'initAsyncCompiler', + 'Compiler', + 'AsyncCompiler', 'Logger', 'SassArgumentList', 'SassBoolean', @@ -79,6 +86,7 @@ void main(List args) { 'FALSE', 'NULL', 'types', + 'NodePackageImporter', }; pkg.githubReleaseNotes.fn = () => @@ -288,3 +296,30 @@ function defaultExportDeprecation() { File("build/npm/sass.node.mjs").writeAsStringSync(buffer.toString()); } + +/// A regular expression to locate the language repo revision in the Dart Sass +/// Homebrew formula. +final _homebrewLanguageRegExp = RegExp( + r'resource "language" do$' + r'(?:(?! end$).)+' + r'revision: "([a-f0-9]{40})"', + dotAll: true, + multiLine: true); + +/// Updates the Homebrew [formula] to change the revision of the language repo +/// to the latest revision. +String _updateHomebrewLanguageRevision(String formula) { + var languageRepoRevision = run("git", + arguments: ["ls-remote", "https://github.com/sass/sass"], quiet: true) + .split("\t") + .first; + + var match = _homebrewLanguageRegExp.firstMatch(formula); + if (match == null) { + fail("Couldn't find a language repo revision in the Homebrew formula."); + } + + return formula.substring(0, match.start) + + match.group(0)!.replaceFirst(match.group(1)!, languageRepoRevision) + + formula.substring(match.end); +}