├── .eslintignore ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── sass.ts ├── buf.gen.yaml ├── buf.work.yaml ├── jest.config.ts ├── lib ├── index.mjs ├── index.ts └── src │ ├── canonicalize-context.ts │ ├── compile.ts │ ├── compiler-path.ts │ ├── compiler.test.ts │ ├── compiler │ ├── async.ts │ ├── sync.ts │ └── utils.ts │ ├── deprecations.ts │ ├── deprotofy-span.ts │ ├── dispatcher.ts │ ├── elf.ts │ ├── exception.ts │ ├── function-registry.ts │ ├── importer-registry.ts │ ├── legacy │ ├── importer.ts │ ├── index.ts │ ├── resolve-path.ts │ ├── utils.ts │ └── value │ │ ├── base.ts │ │ ├── color.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── map.ts │ │ ├── number.ts │ │ ├── string.ts │ │ └── wrap.ts │ ├── logger.ts │ ├── message-transformer.test.ts │ ├── message-transformer.ts │ ├── messages.ts │ ├── packet-transformer.test.ts │ ├── packet-transformer.ts │ ├── protofier.ts │ ├── request-tracker.test.ts │ ├── request-tracker.ts │ ├── utils.test.ts │ ├── utils.ts │ ├── value │ ├── argument-list.ts │ ├── boolean.ts │ ├── calculations.ts │ ├── color.ts │ ├── function.ts │ ├── index.ts │ ├── list.ts │ ├── map.ts │ ├── mixin.ts │ ├── null.ts │ ├── number.ts │ ├── string.ts │ └── utils.ts │ └── version.ts ├── npm ├── android-arm │ ├── README.md │ └── package.json ├── android-arm64 │ ├── README.md │ └── package.json ├── android-riscv64 │ ├── README.md │ └── package.json ├── android-x64 │ ├── README.md │ └── package.json ├── darwin-arm64 │ ├── README.md │ └── package.json ├── darwin-x64 │ ├── README.md │ └── package.json ├── linux-arm │ ├── README.md │ └── package.json ├── linux-arm64 │ ├── README.md │ └── package.json ├── linux-musl-arm │ ├── README.md │ └── package.json ├── linux-musl-arm64 │ ├── README.md │ └── package.json ├── linux-musl-riscv64 │ ├── README.md │ └── package.json ├── linux-musl-x64 │ ├── README.md │ └── package.json ├── linux-riscv64 │ ├── README.md │ └── package.json ├── linux-x64 │ ├── README.md │ └── package.json ├── win32-arm64 │ ├── README.md │ └── package.json └── win32-x64 │ ├── README.md │ └── package.json ├── package.json ├── test ├── after-compile-test.mjs ├── dependencies.test.ts ├── sandbox.ts └── utils.ts ├── tool ├── get-deprecations.ts ├── get-embedded-compiler.ts ├── get-language-repo.ts ├── init.ts ├── prepare-optional-release.ts ├── prepare-release.ts └── utils.ts ├── tsconfig.build.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | language/ 4 | lib/src/vendor/ 5 | **/*.js 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/", 3 | "rules": { 4 | "@typescript-eslint/explicit-function-return-type": [ 5 | "error", 6 | {"allowExpressions": true} 7 | ], 8 | "func-style": ["error", "declaration"], 9 | "prefer-const": ["error", {"destructuring": "all"}], 10 | // It would be nice to sort import declaration order as well, but that's not 11 | // autofixable and it's not worth the effort of handling manually. 12 | "sort-imports": ["error", {"ignoreDeclarationSort": true}], 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | defaults: 4 | run: {shell: bash} 5 | 6 | env: 7 | PROTOC_VERSION: 3.x 8 | 9 | on: 10 | push: 11 | branches: [main, feature.*] 12 | tags: ['**'] 13 | pull_request: 14 | 15 | jobs: 16 | static_analysis: 17 | name: Static analysis 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 'lts/*' 25 | check-latest: true 26 | 27 | - name: Check out the language repo 28 | uses: sass/clone-linked-repo@v1 29 | with: {repo: sass/sass, path: language} 30 | 31 | - run: npm install 32 | - name: npm run init 33 | run: | 34 | npm run init -- --skip-compiler --language-path=language $args 35 | 36 | - run: npm run check 37 | 38 | tests: 39 | name: 'Tests | Node ${{ matrix.node-version }} | ${{ matrix.os }}' 40 | runs-on: ${{ matrix.os }}-latest 41 | 42 | strategy: 43 | matrix: 44 | os: [ubuntu, macos, windows] 45 | node-version: ['lts/*', 'lts/-1', 'lts/-2'] 46 | fail-fast: false 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions/setup-node@v4 51 | with: 52 | node-version: ${{ matrix.node-version }} 53 | check-latest: true 54 | - uses: dart-lang/setup-dart@v1 55 | with: {sdk: stable} 56 | - run: dart --version 57 | 58 | - name: Check out Dart Sass 59 | uses: sass/clone-linked-repo@v1 60 | with: {repo: sass/dart-sass} 61 | 62 | - name: Check out the language repo 63 | uses: sass/clone-linked-repo@v1 64 | with: {repo: sass/sass, path: language} 65 | 66 | - run: npm install 67 | - name: npm run init 68 | run: | 69 | npm run init -- --compiler-path=dart-sass --language-path=language $args 70 | 71 | - run: npm run test 72 | - run: npm run compile 73 | - run: node test/after-compile-test.mjs 74 | 75 | sass_spec: 76 | name: 'JS API Tests | Node ${{ matrix.node_version }} | ${{ matrix.os }}' 77 | runs-on: ${{ matrix.os }}-latest 78 | 79 | strategy: 80 | fail-fast: false 81 | matrix: 82 | os: [ubuntu, windows, macos] 83 | node_version: ['lts/*'] 84 | include: 85 | # Include LTS versions on Ubuntu 86 | - os: ubuntu 87 | node_version: lts/-1 88 | - os: ubuntu 89 | node_version: lts/-2 90 | 91 | steps: 92 | - uses: actions/checkout@v4 93 | - uses: dart-lang/setup-dart@v1 94 | with: {sdk: stable} 95 | - uses: actions/setup-node@v4 96 | with: {node-version: "${{ matrix.node_version }}"} 97 | 98 | - name: Check out Dart Sass 99 | uses: sass/clone-linked-repo@v1 100 | with: {repo: sass/dart-sass} 101 | 102 | - name: Check out the language repo 103 | uses: sass/clone-linked-repo@v1 104 | with: {repo: sass/sass, path: language} 105 | 106 | - run: npm install 107 | - name: npm run init 108 | run: | 109 | npm run init -- --compiler-path=dart-sass --language-path=language $args 110 | 111 | - name: Check out sass-spec 112 | uses: sass/clone-linked-repo@v1 113 | with: {repo: sass/sass-spec} 114 | 115 | - name: Install sass-spec dependencies 116 | run: npm install 117 | working-directory: sass-spec 118 | 119 | - name: Compile 120 | run: | 121 | npm run compile 122 | if [[ "$RUNNER_OS" == "Windows" ]]; then 123 | # Avoid copying the entire Dart Sass build directory on Windows, 124 | # since it may contain symlinks that cp will choke on. 125 | mkdir -p dist/lib/src/vendor/dart-sass/ 126 | cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.bat 127 | cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.snapshot 128 | else 129 | ln -s {`pwd`/,dist/}lib/src/vendor/dart-sass 130 | fi 131 | 132 | - name: Run tests 133 | run: npm run js-api-spec -- --sassPackage .. --sassSassRepo ../language 134 | working-directory: sass-spec 135 | 136 | deploy_npm: 137 | name: Deploy npm 138 | runs-on: ubuntu-latest 139 | if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/embedded-host-node'" 140 | needs: [static_analysis, tests, sass_spec] 141 | 142 | steps: 143 | - uses: actions/checkout@v4 144 | - uses: actions/setup-node@v4 145 | with: 146 | node-version: 'lts/*' 147 | check-latest: true 148 | registry-url: 'https://registry.npmjs.org' 149 | - run: npm install 150 | 151 | - name: "Check we're not using a -dev version of the embedded protocol" 152 | run: jq -r '.["protocol-version"]' package.json | grep -qv -- '-dev$' 153 | - name: "Check we're not using a -dev version of the embedded compiler" 154 | run: jq -r '.["compiler-version"]' package.json | grep -qv -- '-dev$' 155 | 156 | - name: Publish optional dependencies 157 | env: 158 | NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' 159 | run: | 160 | find ./npm -mindepth 1 -maxdepth 1 -print0 | xargs -0 -n 1 -- sh -xc 'npx ts-node ./tool/prepare-optional-release.ts --package=$(basename $1) && npm publish $1' -- 161 | 162 | - run: npm publish 163 | env: 164 | NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' 165 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | _types 3 | build 4 | dist 5 | lib/src/vendor 6 | node_modules 7 | npm-debug.log* 8 | package-lock.json 9 | test/sandbox 10 | npm/*/dart-sass/ 11 | 12 | # Editors 13 | .idea 14 | .vscode 15 | *.njsproj 16 | *.ntvs* 17 | *.sln 18 | *.suo 19 | *.sw? 20 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json'), 3 | }; 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Sass is more than a technology; Sass is driven by the community of individuals 2 | that power its development and use every day. As a community, we want to embrace 3 | the very differences that have made our collaboration so powerful, and work 4 | together to provide the best environment for learning, growing, and sharing of 5 | ideas. It is imperative that we keep Sass a fun, welcoming, challenging, and 6 | fair place to play. 7 | 8 | [The full community guidelines can be found on the Sass website.][link] 9 | 10 | [link]: http://sass-lang.com/community-guidelines 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | * [Contributor License Agreement](#contributor-license-agreement) 7 | * [Code Reviews](#code-reviews) 8 | * [Large Language Models](#large-language-models) 9 | * [Release Process](#release-process) 10 | * [Keeping in Sync With Other Packages](#keeping-in-sync-with-other-packages) 11 | * [Local Development](#local-development) 12 | * [Continuous Integration](#continuous-integration) 13 | * [Release](#release) 14 | 15 | ## Contributor License Agreement 16 | 17 | Contributions to this project must be accompanied by a Contributor License 18 | Agreement. You (or your employer) retain the copyright to your contribution; 19 | this simply gives us permission to use and redistribute your contributions as 20 | part of the project. Head over to to see 21 | your current agreements on file or to sign a new one. 22 | 23 | You generally only need to submit a CLA once, so if you've already submitted one 24 | (even if it was for a different project), you probably don't need to do it 25 | again. 26 | 27 | ## Code Reviews 28 | 29 | All submissions, including submissions by project members, require review. We 30 | use GitHub pull requests for this purpose. Consult 31 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 32 | information on using pull requests. 33 | 34 | ## Large Language Models 35 | 36 | Do not submit any code or prose written or modified by large language models or 37 | "artificial intelligence" such as GitHub Copilot or ChatGPT to this project. 38 | These tools produce code that looks plausible, which means that not only is it 39 | likely to contain bugs those bugs are likely to be difficult to notice on 40 | review. In addition, because these models were trained indiscriminately and 41 | non-consensually on open-source code with a variety of licenses, it's not 42 | obvious that we have the moral or legal right to redistribute code they 43 | generate. 44 | 45 | ## Release process 46 | 47 | Because this package's version remains in lockstep with the current version of 48 | Dart Sass, it's not released manually from this repository. Instead, a release 49 | commit is automatically generated once a new Embedded Dart Sass version has been 50 | released, which in turn is automatically triggered by a release of Dart Sass. As 51 | such, manual commits should never: 52 | 53 | * Update the `package.json`'s version to a non-`-dev` number. Changing it from 54 | non-`-dev` to dev when adding a new feature is fine. 55 | 56 | * Update the `package.json`'s `"compiler-version"` field to a non-`-dev` number. 57 | Changing it from non-`-dev` to dev when using a new feature is fine. 58 | 59 | ## Keeping in Sync With Other Packages 60 | 61 | The embedded host depends on several different components which come from 62 | different repositories: 63 | 64 | * The [Dart Sass compiler]. 65 | * The [Sass embedded protocol]. 66 | * The [Sass JS API definition]. 67 | 68 | [Dart Sass compiler]: https://github.com/sass/dart-sass 69 | [Sass embedded protocol]: https://github.com/sass/sass/tree/main/spec/embedded-protocol.md 70 | [JS API definition]: https://github.com/sass/sass/tree/main/spec/js-api 71 | 72 | These dependencies are made available in different ways depending on context. 73 | 74 | ### Local Development 75 | 76 | When developing locally, you can download all of these dependencies by running 77 | `npm install` and then `npm run init`. This provides the following options for 78 | `compiler` (for the embedded compiler), `protocol` (for the embedded protocol), 79 | and `api` (for the JS API): 80 | 81 | * `---path`: The local filesystem path of the package to use. This is 82 | useful when doing local development on both the host and its dependencies at 83 | the same time. 84 | 85 | * `---ref`: A Git reference for the GitHub repository of the package to 86 | clone. 87 | 88 | If developing locally, you will need to specify both the compiler and language. 89 | For example: `npm run init -- --compiler-path=dart-sass --language-path=language`. 90 | 91 | By default: 92 | 93 | * This uses the version of the embedded protocol and compiler specified by 94 | `protocol-version` in `package.json`, *unless* that version ends in `-dev` in 95 | which case it checks out the latest revision on GitHub. 96 | 97 | * This uses the embedded compiler version and JS API definition from the latest 98 | revision on GitHub. 99 | 100 | * This uses the Dart Sass version from the latest revision on GitHub, unless the 101 | `--compiler-path` was passed in which case it uses that version of Dart Sass. 102 | 103 | ### Continuous Integration 104 | 105 | CI tests also use `npm run init`, so they use the same defaults as local 106 | development. However, if the pull request description includes a link to a pull 107 | request for Dart Sass, the embedded protocol, or the JS API, this will check out 108 | that version and run tests against it instead. 109 | 110 | ### Release 111 | 112 | When this package is released to npm, it downloads the embedded protocol version 113 | that matches `protocol-version` in `package.json`. It downloads the latest JS 114 | API revision on GitHub. 115 | 116 | The release version of the `sass-embedded` package does *not* include Dart Sass. 117 | Instead, we release optional packages of the form `sass-embedded--`. 118 | Each of these contains the published version of Dart Sass that matches 119 | `compiler-version` in `package.json` for the given operating system/architecture 120 | combination. 121 | 122 | If either `protocol-version` or `compiler-version` ends with `-dev`, the release 123 | will fail. 124 | 125 | **Note:** As part of the holistic release process for Dart Sass, the embedded 126 | compiler's CI will automatically update this repository's `package.json` file 127 | with the latest `compiler-version` and optional dependency versions before 128 | tagging it for a release. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Google LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Embedded Sass Host 2 | 3 | This package is an alternative to the [`sass`] package. It supports the same JS 4 | API as `sass` and is maintained by the same team, but where the `sass` package 5 | is pure JavaScript, `sass-embedded` is instead a JavaScript wrapper around a 6 | native Dart executable. This means `sass-embedded` will generally be much faster 7 | especially for large Sass compilations, but it can only be installed on the 8 | platforms that Dart supports: Windows, Mac OS, and Linux. 9 | 10 | [`sass`]: https://www.npmjs.com/package/sass 11 | 12 | Despite being different packages, both `sass` and `sass-embedded` are considered 13 | "Dart Sass" since they have the same underlying implementation. Since the first 14 | stable release of the `sass-embedded` package, both packages are released at the 15 | same time and share the same version number. 16 | 17 | ## Usage 18 | 19 | This package provides the same JavaScript API as the `sass` package, and can be 20 | used as a drop-in replacement: 21 | 22 | ```js 23 | const sass = require('sass-embedded'); 24 | 25 | const result = sass.compile(scssFilename); 26 | 27 | // OR 28 | 29 | const result = await sass.compileAsync(scssFilename); 30 | ``` 31 | 32 | Unlike the `sass` package, the asynchronous API in `sass-embedded` will 33 | generally be faster than the synchronous API since the Sass compilation logic is 34 | happening in a different process. 35 | 36 | See [the Sass website] for full API documentation. 37 | 38 | [the Sass website]: https://sass-lang.com/documentation/js-api 39 | 40 | ### Legacy API 41 | 42 | The `sass-embedded` package also supports the older JavaScript API that's fully 43 | compatible with [Node Sass] (with a few exceptions listed below), with support 44 | for both the [`render()`] and [`renderSync()`] functions. This API is considered 45 | deprecated and will be removed in Dart Sass 2.0.0, so it should be avoided in 46 | new projects. 47 | 48 | [Node Sass]: https://github.com/sass/node-sass 49 | [`render()`]: https://sass-lang.com/documentation/js-api/modules#render 50 | [`renderSync()`]: https://sass-lang.com/documentation/js-api/modules#renderSync 51 | 52 | Sass's support for the legacy JavaScript API has the following limitations: 53 | 54 | * Only the `"expanded"` and `"compressed"` values of [`outputStyle`] are 55 | supported. 56 | 57 | * The `sass-embedded` package doesn't support the [`precision`] option. Dart 58 | Sass defaults to a sufficiently high precision for all existing browsers, and 59 | making this customizable would make the code substantially less efficient. 60 | 61 | * The `sass-embedded` package doesn't support the [`sourceComments`] option. 62 | Source maps are the recommended way of locating the origin of generated 63 | selectors. 64 | 65 | * The `sass-embedded` package doesn't support the [`indentWidth`], 66 | [`indentType`], or [`linefeed`] options. It implements the legacy API as a 67 | wrapper around the new API, and the new API has dropped support for these 68 | options. 69 | 70 | [`outputStyle`]: https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#outputStyle 71 | [`precision`]: https://github.com/sass/node-sass#precision 72 | [`indentWidth`]: https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#indentWidth 73 | [`indentType`]: https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#indentType 74 | [`linefeed`]: https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#linefeed 75 | 76 | ## How Does It Work? 77 | 78 | The `sass-embedded` runs the Dart Sass [embedded compiler] as a separate 79 | executable and uses the [Embedded Sass Protocol] to communicate with it over its 80 | stdin and stdout streams. This protocol is designed to make it possible not only 81 | to start a Sass compilation, but to control aspects of it that are exposed by an 82 | API. This includes defining custom importers, functions, and loggers, all of 83 | which are invoked by messages from the embedded compiler back to the host. 84 | 85 | [embedded compiler]: https://github.com/sass/dart-sass#embedded-dart-sass 86 | [Embedded Sass Protocol]: https://github.com/sass/sass/tree/main/spec/embedded-protocol.md 87 | 88 | Although this sort of two-way communication with an embedded process is 89 | inherently asynchronous in Node.js, this package supports the synchronous 90 | `compile()` API using a custom [synchronous message-passing library] that's 91 | implemented with the [`Atomics.wait()`] primitive. 92 | 93 | [synchronous message-passing library]: https://github.com/sass/sync-message-port 94 | [`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait 95 | 96 | --- 97 | 98 | Disclaimer: this is not an official Google product. 99 | -------------------------------------------------------------------------------- /bin/sass.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as child_process from 'child_process'; 4 | import {compilerCommand} from '../lib/src/compiler-path'; 5 | 6 | // TODO npm/cmd-shim#152 and yarnpkg/berry#6422 - If and when the package 7 | // managers support it, we should make this a proper shell script rather than a 8 | // JS wrapper. 9 | 10 | try { 11 | child_process.execFileSync( 12 | compilerCommand[0], 13 | [...compilerCommand.slice(1), ...process.argv.slice(2)], 14 | { 15 | stdio: 'inherit', 16 | windowsHide: true, 17 | }, 18 | ); 19 | } catch (error) { 20 | if (error.code) { 21 | throw error; 22 | } else { 23 | process.exitCode = error.status; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: es 4 | out: lib/src/vendor 5 | opt: target=ts 6 | -------------------------------------------------------------------------------- /buf.work.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | directories: [build/sass/spec] 3 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | roots: ['/lib/', '/test/'], 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /lib/index.mjs: -------------------------------------------------------------------------------- 1 | import * as sass from './index.js'; 2 | 3 | export const compile = sass.compile; 4 | export const compileAsync = sass.compileAsync; 5 | export const compileString = sass.compileString; 6 | export const compileStringAsync = sass.compileStringAsync; 7 | export const NodePackageImporter = sass.NodePackageImporter; 8 | export const AsyncCompiler = sass.AsyncCompiler; 9 | export const Compiler = sass.Compiler; 10 | export const initAsyncCompiler = sass.initAsyncCompiler; 11 | export const initCompiler = sass.initCompiler; 12 | export const deprecations = sass.deprecations; 13 | export const Version = sass.Version; 14 | export const Logger = sass.Logger; 15 | export const CalculationInterpolation = sass.CalculationInterpolation; 16 | export const CalculationOperation = sass.CalculationOperation; 17 | export const CalculationOperator = sass.CalculationOperator; 18 | export const SassArgumentList = sass.SassArgumentList; 19 | export const SassBoolean = sass.SassBoolean; 20 | export const SassCalculation = sass.SassCalculation; 21 | export const SassColor = sass.SassColor; 22 | export const SassFunction = sass.SassFunction; 23 | export const SassMixin = sass.SassMixin; 24 | export const SassList = sass.SassList; 25 | export const SassMap = sass.SassMap; 26 | export const SassNumber = sass.SassNumber; 27 | export const SassString = sass.SassString; 28 | export const Value = sass.Value; 29 | export const CustomFunction = sass.CustomFunction; 30 | export const ListSeparator = sass.ListSeparator; 31 | export const sassFalse = sass.sassFalse; 32 | export const sassNull = sass.sassNull; 33 | export const sassTrue = sass.sassTrue; 34 | export const Exception = sass.Exception; 35 | export const PromiseOr = sass.PromiseOr; 36 | export const info = sass.info; 37 | export const render = sass.render; 38 | export const renderSync = sass.renderSync; 39 | export const TRUE = sass.TRUE; 40 | export const FALSE = sass.FALSE; 41 | export const NULL = sass.NULL; 42 | export const types = sass.types; 43 | 44 | let printedDefaultExportDeprecation = false; 45 | function defaultExportDeprecation() { 46 | if (printedDefaultExportDeprecation) return; 47 | printedDefaultExportDeprecation = true; 48 | console.error( 49 | "`import sass from 'sass'` is deprecated.\n" + 50 | "Please use `import * as sass from 'sass'` instead." 51 | ); 52 | } 53 | 54 | export default { 55 | get compile() { 56 | defaultExportDeprecation(); 57 | return sass.compile; 58 | }, 59 | get compileAsync() { 60 | defaultExportDeprecation(); 61 | return sass.compileAsync; 62 | }, 63 | get compileString() { 64 | defaultExportDeprecation(); 65 | return sass.compileString; 66 | }, 67 | get compileStringAsync() { 68 | defaultExportDeprecation(); 69 | return sass.compileStringAsync; 70 | }, 71 | get NodePackageImporter() { 72 | defaultExportDeprecation(); 73 | return sass.NodePackageImporter; 74 | }, 75 | get initAsyncCompiler() { 76 | defaultExportDeprecation(); 77 | return sass.initAsyncCompiler; 78 | }, 79 | get initCompiler() { 80 | defaultExportDeprecation(); 81 | return sass.initCompiler; 82 | }, 83 | get AsyncCompiler() { 84 | defaultExportDeprecation(); 85 | return sass.AsyncCompiler; 86 | }, 87 | get Compiler() { 88 | defaultExportDeprecation(); 89 | return sass.Compiler; 90 | }, 91 | get deprecations() { 92 | defaultExportDeprecation(); 93 | return sass.deprecations; 94 | }, 95 | get Version() { 96 | defaultExportDeprecation(); 97 | return sass.Version; 98 | }, 99 | get Logger() { 100 | defaultExportDeprecation(); 101 | return sass.Logger; 102 | }, 103 | get CalculationOperation() { 104 | defaultExportDeprecation(); 105 | return sass.CalculationOperation; 106 | }, 107 | get CalculationOperator() { 108 | defaultExportDeprecation(); 109 | return sass.CalculationOperator; 110 | }, 111 | get CalculationInterpolation() { 112 | defaultExportDeprecation(); 113 | return sass.CalculationInterpolation; 114 | }, 115 | get SassArgumentList() { 116 | defaultExportDeprecation(); 117 | return sass.SassArgumentList; 118 | }, 119 | get SassBoolean() { 120 | defaultExportDeprecation(); 121 | return sass.SassBoolean; 122 | }, 123 | get SassCalculation() { 124 | defaultExportDeprecation(); 125 | return sass.SassCalculation; 126 | }, 127 | get SassColor() { 128 | defaultExportDeprecation(); 129 | return sass.SassColor; 130 | }, 131 | get SassFunction() { 132 | defaultExportDeprecation(); 133 | return sass.SassFunction; 134 | }, 135 | get SassMixin() { 136 | defaultExportDeprecation(); 137 | return sass.SassMixin; 138 | }, 139 | get SassList() { 140 | defaultExportDeprecation(); 141 | return sass.SassList; 142 | }, 143 | get SassMap() { 144 | defaultExportDeprecation(); 145 | return sass.SassMap; 146 | }, 147 | get SassNumber() { 148 | defaultExportDeprecation(); 149 | return sass.SassNumber; 150 | }, 151 | get SassString() { 152 | defaultExportDeprecation(); 153 | return sass.SassString; 154 | }, 155 | get Value() { 156 | defaultExportDeprecation(); 157 | return sass.Value; 158 | }, 159 | get CustomFunction() { 160 | defaultExportDeprecation(); 161 | return sass.CustomFunction; 162 | }, 163 | get ListSeparator() { 164 | defaultExportDeprecation(); 165 | return sass.ListSeparator; 166 | }, 167 | get sassFalse() { 168 | defaultExportDeprecation(); 169 | return sass.sassFalse; 170 | }, 171 | get sassNull() { 172 | defaultExportDeprecation(); 173 | return sass.sassNull; 174 | }, 175 | get sassTrue() { 176 | defaultExportDeprecation(); 177 | return sass.sassTrue; 178 | }, 179 | get Exception() { 180 | defaultExportDeprecation(); 181 | return sass.Exception; 182 | }, 183 | get PromiseOr() { 184 | defaultExportDeprecation(); 185 | return sass.PromiseOr; 186 | }, 187 | get info() { 188 | defaultExportDeprecation(); 189 | return sass.info; 190 | }, 191 | get render() { 192 | defaultExportDeprecation(); 193 | return sass.render; 194 | }, 195 | get renderSync() { 196 | defaultExportDeprecation(); 197 | return sass.renderSync; 198 | }, 199 | get TRUE() { 200 | defaultExportDeprecation(); 201 | return sass.TRUE; 202 | }, 203 | get FALSE() { 204 | defaultExportDeprecation(); 205 | return sass.FALSE; 206 | }, 207 | get NULL() { 208 | defaultExportDeprecation(); 209 | return sass.NULL; 210 | }, 211 | get types() { 212 | defaultExportDeprecation(); 213 | return sass.types; 214 | }, 215 | }; 216 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as pkg from '../package.json'; 6 | import {sassFalse, sassTrue} from './src/value/boolean'; 7 | import {sassNull} from './src/value/null'; 8 | 9 | export {ListSeparator, SassList} from './src/value/list'; 10 | export {SassArgumentList} from './src/value/argument-list'; 11 | export {SassBoolean, sassFalse, sassTrue} from './src/value/boolean'; 12 | export {SassColor} from './src/value/color'; 13 | export {SassFunction} from './src/value/function'; 14 | export {SassMap} from './src/value/map'; 15 | export {SassMixin} from './src/value/mixin'; 16 | export {SassNumber} from './src/value/number'; 17 | export {SassString} from './src/value/string'; 18 | export {Value} from './src/value'; 19 | export {sassNull} from './src/value/null'; 20 | export { 21 | CalculationOperation, 22 | CalculationOperator, 23 | CalculationInterpolation, 24 | SassCalculation, 25 | } from './src/value/calculations'; 26 | 27 | export * as types from './src/legacy/value'; 28 | export {Exception} from './src/exception'; 29 | export { 30 | compile, 31 | compileString, 32 | compileAsync, 33 | compileStringAsync, 34 | NodePackageImporter, 35 | } from './src/compile'; 36 | export {initAsyncCompiler, AsyncCompiler} from './src/compiler/async'; 37 | export {initCompiler, Compiler} from './src/compiler/sync'; 38 | export { 39 | deprecations, 40 | Deprecation, 41 | DeprecationOrId, 42 | DeprecationStatus, 43 | } from './src/deprecations'; 44 | export {Version} from './src/version'; 45 | export {render, renderSync} from './src/legacy'; 46 | 47 | export const info = `sass-embedded\t${pkg.version}`; 48 | 49 | export {Logger} from './src/logger'; 50 | 51 | // Legacy JS API 52 | 53 | export const TRUE = sassTrue; 54 | export const FALSE = sassFalse; 55 | export const NULL = sassNull; 56 | -------------------------------------------------------------------------------- /lib/src/canonicalize-context.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | export class CanonicalizeContext { 6 | readonly fromImport: boolean; 7 | 8 | private readonly _containingUrl: URL | null; 9 | 10 | get containingUrl(): URL | null { 11 | this._containingUrlAccessed = true; 12 | return this._containingUrl; 13 | } 14 | 15 | private _containingUrlAccessed = false; 16 | 17 | /** 18 | * Whether the `containingUrl` getter has been accessed. 19 | * 20 | * This is marked as public so that the importer registry can access it, but 21 | * it's not part of the package's public API and should not be accessed by 22 | * user code. It may be renamed or removed without warning in the future. 23 | */ 24 | get containingUrlAccessed(): boolean { 25 | return this._containingUrlAccessed; 26 | } 27 | 28 | constructor(containingUrl: URL | null, fromImport: boolean) { 29 | this._containingUrl = containingUrl; 30 | this.fromImport = fromImport; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/compile.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {initAsyncCompiler} from './compiler/async'; 6 | import {OptionsWithLegacy, StringOptionsWithLegacy} from './compiler/utils'; 7 | import {initCompiler} from './compiler/sync'; 8 | import {CompileResult} from './vendor/sass'; 9 | 10 | export {NodePackageImporter} from './importer-registry'; 11 | 12 | export function compile( 13 | path: string, 14 | options?: OptionsWithLegacy<'sync'>, 15 | ): CompileResult { 16 | const compiler = initCompiler(); 17 | try { 18 | return compiler.compile(path, options); 19 | } finally { 20 | compiler.dispose(); 21 | } 22 | } 23 | 24 | export function compileString( 25 | source: string, 26 | options?: StringOptionsWithLegacy<'sync'>, 27 | ): CompileResult { 28 | const compiler = initCompiler(); 29 | try { 30 | return compiler.compileString(source, options); 31 | } finally { 32 | compiler.dispose(); 33 | } 34 | } 35 | 36 | export async function compileAsync( 37 | path: string, 38 | options?: OptionsWithLegacy<'async'>, 39 | ): Promise { 40 | const compiler = await initAsyncCompiler(); 41 | try { 42 | return await compiler.compileAsync(path, options); 43 | } finally { 44 | await compiler.dispose(); 45 | } 46 | } 47 | 48 | export async function compileStringAsync( 49 | source: string, 50 | options?: StringOptionsWithLegacy<'async'>, 51 | ): Promise { 52 | const compiler = await initAsyncCompiler(); 53 | try { 54 | return await compiler.compileStringAsync(source, options); 55 | } finally { 56 | await compiler.dispose(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/compiler-path.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as fs from 'fs'; 6 | import * as p from 'path'; 7 | import {getElfInterpreter} from './elf'; 8 | import {isErrnoException} from './utils'; 9 | 10 | /** 11 | * Detect if the given binary is linked with musl libc by checking if 12 | * the interpreter basename starts with "ld-musl-" 13 | */ 14 | function isLinuxMusl(path: string): boolean { 15 | try { 16 | const interpreter = getElfInterpreter(path); 17 | return p.basename(interpreter).startsWith('ld-musl-'); 18 | } catch (error) { 19 | console.warn( 20 | `Warning: Failed to detect linux-musl, fallback to linux-gnu: ${error.message}`, 21 | ); 22 | return false; 23 | } 24 | } 25 | 26 | /** The full command for the embedded compiler executable. */ 27 | export const compilerCommand = (() => { 28 | const platform = 29 | process.platform === 'linux' && isLinuxMusl(process.execPath) 30 | ? 'linux-musl' 31 | : (process.platform as string); 32 | 33 | const arch = process.arch; 34 | 35 | // find for development 36 | for (const path of ['vendor', '../../../lib/src/vendor']) { 37 | const executable = p.resolve( 38 | __dirname, 39 | path, 40 | `dart-sass/sass${platform === 'win32' ? '.bat' : ''}`, 41 | ); 42 | 43 | if (fs.existsSync(executable)) return [executable]; 44 | } 45 | 46 | try { 47 | return [ 48 | require.resolve( 49 | `sass-embedded-${platform}-${arch}/dart-sass/src/dart` + 50 | (platform === 'win32' ? '.exe' : ''), 51 | ), 52 | require.resolve( 53 | `sass-embedded-${platform}-${arch}/dart-sass/src/sass.snapshot`, 54 | ), 55 | ]; 56 | } catch (ignored) { 57 | // ignored 58 | } 59 | 60 | try { 61 | return [ 62 | require.resolve( 63 | `sass-embedded-${platform}-${arch}/dart-sass/sass` + 64 | (platform === 'win32' ? '.bat' : ''), 65 | ), 66 | ]; 67 | } catch (e: unknown) { 68 | if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) { 69 | throw e; 70 | } 71 | } 72 | 73 | throw new Error( 74 | "Embedded Dart Sass couldn't find the embedded compiler executable. " + 75 | 'Please make sure the optional dependency ' + 76 | `sass-embedded-${platform}-${arch} is installed in ` + 77 | 'node_modules.', 78 | ); 79 | })(); 80 | -------------------------------------------------------------------------------- /lib/src/compiler.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as fs from 'fs'; 6 | import {chdir} from 'process'; 7 | import {AsyncCompiler, initAsyncCompiler} from './compiler/async'; 8 | import * as compilerModule from './compiler/utils'; 9 | import {Compiler, initCompiler} from './compiler/sync'; 10 | 11 | const createDispatcher = jest.spyOn(compilerModule, 'createDispatcher'); 12 | function getIdHistory(): number[] { 13 | return createDispatcher.mock.calls.map(([id]) => id); 14 | } 15 | 16 | afterEach(() => { 17 | createDispatcher.mockClear(); 18 | }); 19 | 20 | describe('compiler', () => { 21 | let compiler: Compiler; 22 | const importers = [ 23 | { 24 | canonicalize: () => new URL('foo:bar'), 25 | load: () => ({ 26 | contents: compiler.compileString('').css, 27 | syntax: 'scss' as const, 28 | }), 29 | }, 30 | ]; 31 | 32 | beforeEach(() => { 33 | compiler = initCompiler(); 34 | }); 35 | 36 | afterEach(() => { 37 | compiler.dispose(); 38 | }); 39 | 40 | it('calls functions independently', () => { 41 | const [logger1, logger2] = [jest.fn(), jest.fn()]; 42 | compiler.compileString('@debug ""', {logger: {debug: logger1}}); 43 | compiler.compileString('@debug ""', {logger: {debug: logger2}}); 44 | expect(logger1).toHaveBeenCalledTimes(1); 45 | expect(logger2).toHaveBeenCalledTimes(1); 46 | }); 47 | 48 | it('handles the removal of the working directory', () => { 49 | const oldDir = fs.mkdtempSync('sass-spec-'); 50 | chdir(oldDir); 51 | const tmpCompiler = initCompiler(); 52 | chdir('..'); 53 | fs.rmSync(oldDir, {recursive: true}); 54 | fs.writeFileSync('foo.scss', 'a {b: c}'); 55 | expect(() => tmpCompiler.compile('foo.scss')).not.toThrow(); 56 | tmpCompiler.dispose(); 57 | fs.rmSync('foo.scss'); 58 | }); 59 | 60 | describe('compilation ID', () => { 61 | it('resets after callback compilations complete', () => { 62 | compiler.compileString('@use "foo"', {importers}); 63 | compiler.compileString(''); 64 | expect(getIdHistory()).toEqual([1, 2, 1]); 65 | }); 66 | 67 | it('keeps working after failed compilations', () => { 68 | expect(() => compiler.compileString('invalid')).toThrow(); 69 | compiler.compileString('@use "foo"', {importers}); 70 | expect(getIdHistory()).toEqual([1, 1, 2]); 71 | }); 72 | }); 73 | }); 74 | 75 | describe('asyncCompiler', () => { 76 | let asyncCompiler: AsyncCompiler; 77 | 78 | beforeEach(async () => { 79 | asyncCompiler = await initAsyncCompiler(); 80 | }); 81 | 82 | afterEach(async () => { 83 | await asyncCompiler.dispose(); 84 | }); 85 | 86 | it('handles the removal of the working directory', async () => { 87 | const oldDir = fs.mkdtempSync('sass-spec-'); 88 | chdir(oldDir); 89 | const tmpCompiler = await initAsyncCompiler(); 90 | chdir('..'); 91 | fs.rmSync(oldDir, {recursive: true}); 92 | fs.writeFileSync('foo.scss', 'a {b: c}'); 93 | await expect(tmpCompiler.compileAsync('foo.scss')).resolves.not.toThrow(); 94 | await tmpCompiler.dispose(); 95 | fs.rmSync('foo.scss'); 96 | }); 97 | 98 | it('calls functions independently', async () => { 99 | const [logger1, logger2] = [jest.fn(), jest.fn()]; 100 | await asyncCompiler.compileStringAsync('@debug ""', { 101 | logger: {debug: logger1}, 102 | }); 103 | await asyncCompiler.compileStringAsync('@debug ""', { 104 | logger: {debug: logger2}, 105 | }); 106 | expect(logger1).toHaveBeenCalledTimes(1); 107 | expect(logger2).toHaveBeenCalledTimes(1); 108 | }); 109 | 110 | describe('compilation ID', () => { 111 | it('resets after concurrent compilations complete', async () => { 112 | await Promise.all( 113 | Array.from({length: 10}, () => asyncCompiler.compileStringAsync('')), 114 | ); 115 | await asyncCompiler.compileStringAsync(''); 116 | expect(getIdHistory()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1]); 117 | }); 118 | 119 | it('keeps working after failed compilations', async () => { 120 | await expect( 121 | asyncCompiler.compileStringAsync('invalid'), 122 | ).rejects.toThrow(); 123 | await Promise.all([ 124 | asyncCompiler.compileStringAsync(''), 125 | asyncCompiler.compileStringAsync(''), 126 | ]); 127 | expect(getIdHistory()).toEqual([1, 1, 2]); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /lib/src/compiler/async.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {spawn} from 'child_process'; 6 | import {Observable} from 'rxjs'; 7 | import {takeUntil} from 'rxjs/operators'; 8 | 9 | import * as path from 'path'; 10 | import { 11 | OptionsWithLegacy, 12 | StringOptionsWithLegacy, 13 | createDispatcher, 14 | handleCompileResponse, 15 | handleLogEvent, 16 | newCompilePathRequest, 17 | newCompileStringRequest, 18 | } from './utils'; 19 | import {compilerCommand} from '../compiler-path'; 20 | import {activeDeprecationOptions} from '../deprecations'; 21 | import {FunctionRegistry} from '../function-registry'; 22 | import {ImporterRegistry} from '../importer-registry'; 23 | import {MessageTransformer} from '../message-transformer'; 24 | import {PacketTransformer} from '../packet-transformer'; 25 | import * as utils from '../utils'; 26 | import * as proto from '../vendor/embedded_sass_pb'; 27 | import {CompileResult} from '../vendor/sass'; 28 | 29 | /** 30 | * Flag allowing the constructor passed by `initAsyncCompiler` so we can 31 | * differentiate and throw an error if the `AsyncCompiler` is constructed via 32 | * `new AsyncCompiler`. 33 | */ 34 | const initFlag = Symbol(); 35 | 36 | /** An asynchronous wrapper for the embedded Sass compiler */ 37 | export class AsyncCompiler { 38 | /** The underlying process that's being wrapped. */ 39 | private readonly process = spawn( 40 | compilerCommand[0], 41 | [...compilerCommand.slice(1), '--embedded'], 42 | { 43 | // Use the command's cwd so the compiler survives the removal of the 44 | // current working directory. 45 | // https://github.com/sass/embedded-host-node/pull/261#discussion_r1438712923 46 | cwd: path.dirname(compilerCommand[0]), 47 | // Node blocks launching .bat and .cmd without a shell due to CVE-2024-27980 48 | shell: ['.bat', '.cmd'].includes( 49 | path.extname(compilerCommand[0]).toLowerCase(), 50 | ), 51 | windowsHide: true, 52 | }, 53 | ); 54 | 55 | /** The next compilation ID. */ 56 | private compilationId = 1; 57 | 58 | /** A list of active compilations. */ 59 | private readonly compilations: Set< 60 | Promise 61 | > = new Set(); 62 | 63 | /** Whether the underlying compiler has already exited. */ 64 | private disposed = false; 65 | 66 | /** Reusable message transformer for all compilations. */ 67 | private readonly messageTransformer: MessageTransformer; 68 | 69 | /** The child process's exit event. */ 70 | private readonly exit$ = new Promise(resolve => { 71 | this.process.on('exit', code => resolve(code)); 72 | }); 73 | 74 | /** The buffers emitted by the child process's stdout. */ 75 | private readonly stdout$ = new Observable(observer => { 76 | this.process.stdout.on('data', buffer => observer.next(buffer)); 77 | }).pipe(takeUntil(this.exit$)); 78 | 79 | /** The buffers emitted by the child process's stderr. */ 80 | private readonly stderr$ = new Observable(observer => { 81 | this.process.stderr.on('data', buffer => observer.next(buffer)); 82 | }).pipe(takeUntil(this.exit$)); 83 | 84 | /** Writes `buffer` to the child process's stdin. */ 85 | private writeStdin(buffer: Buffer): void { 86 | this.process.stdin.write(buffer); 87 | } 88 | 89 | /** Guards against using a disposed compiler. */ 90 | private throwIfDisposed(): void { 91 | if (this.disposed) { 92 | throw utils.compilerError('Async compiler has already been disposed.'); 93 | } 94 | } 95 | 96 | /** 97 | * Sends a compile request to the child process and returns a Promise that 98 | * resolves with the CompileResult. Rejects the promise if there were any 99 | * protocol or compilation errors. 100 | */ 101 | private async compileRequestAsync( 102 | request: proto.InboundMessage_CompileRequest, 103 | importers: ImporterRegistry<'async'>, 104 | options?: OptionsWithLegacy<'async'> & {legacy?: boolean}, 105 | ): Promise { 106 | const optionsKey = Symbol(); 107 | activeDeprecationOptions.set(optionsKey, options ?? {}); 108 | try { 109 | const functions = new FunctionRegistry(options?.functions); 110 | 111 | const dispatcher = createDispatcher<'async'>( 112 | this.compilationId++, 113 | this.messageTransformer, 114 | { 115 | handleImportRequest: request => importers.import(request), 116 | handleFileImportRequest: request => importers.fileImport(request), 117 | handleCanonicalizeRequest: request => importers.canonicalize(request), 118 | handleFunctionCallRequest: request => functions.call(request), 119 | }, 120 | ); 121 | dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); 122 | 123 | const compilation = new Promise( 124 | (resolve, reject) => 125 | dispatcher.sendCompileRequest(request, (err, response) => { 126 | this.compilations.delete(compilation); 127 | // Reset the compilation ID when the compiler goes idle (no active 128 | // compilations) to avoid overflowing it. 129 | // https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794 130 | if (this.compilations.size === 0) this.compilationId = 1; 131 | if (err) { 132 | reject(err); 133 | } else { 134 | resolve(response!); 135 | } 136 | }), 137 | ); 138 | this.compilations.add(compilation); 139 | 140 | return handleCompileResponse(await compilation); 141 | } finally { 142 | activeDeprecationOptions.delete(optionsKey); 143 | } 144 | } 145 | 146 | /** Initialize resources shared across compilations. */ 147 | constructor(flag: Symbol | undefined) { 148 | if (flag !== initFlag) { 149 | throw utils.compilerError( 150 | 'AsyncCompiler can not be directly constructed. ' + 151 | 'Please use `sass.initAsyncCompiler()` instead.', 152 | ); 153 | } 154 | this.stderr$.subscribe(data => process.stderr.write(data)); 155 | const packetTransformer = new PacketTransformer(this.stdout$, buffer => { 156 | this.writeStdin(buffer); 157 | }); 158 | this.messageTransformer = new MessageTransformer( 159 | packetTransformer.outboundProtobufs$, 160 | packet => packetTransformer.writeInboundProtobuf(packet), 161 | ); 162 | } 163 | 164 | compileAsync( 165 | path: string, 166 | options?: OptionsWithLegacy<'async'>, 167 | ): Promise { 168 | this.throwIfDisposed(); 169 | const importers = new ImporterRegistry(options); 170 | return this.compileRequestAsync( 171 | newCompilePathRequest(path, importers, options), 172 | importers, 173 | options, 174 | ); 175 | } 176 | 177 | compileStringAsync( 178 | source: string, 179 | options?: StringOptionsWithLegacy<'async'>, 180 | ): Promise { 181 | this.throwIfDisposed(); 182 | const importers = new ImporterRegistry(options); 183 | return this.compileRequestAsync( 184 | newCompileStringRequest(source, importers, options), 185 | importers, 186 | options, 187 | ); 188 | } 189 | 190 | async dispose(): Promise { 191 | this.disposed = true; 192 | await Promise.all(this.compilations); 193 | this.process.stdin.end(); 194 | await this.exit$; 195 | } 196 | } 197 | 198 | export async function initAsyncCompiler(): Promise { 199 | return new AsyncCompiler(initFlag); 200 | } 201 | -------------------------------------------------------------------------------- /lib/src/compiler/sync.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Subject} from 'rxjs'; 6 | import {SyncChildProcess} from 'sync-child-process'; 7 | 8 | import * as path from 'path'; 9 | import { 10 | OptionsWithLegacy, 11 | createDispatcher, 12 | handleCompileResponse, 13 | handleLogEvent, 14 | newCompilePathRequest, 15 | newCompileStringRequest, 16 | } from './utils'; 17 | import {compilerCommand} from '../compiler-path'; 18 | import {activeDeprecationOptions} from '../deprecations'; 19 | import {Dispatcher} from '../dispatcher'; 20 | import {FunctionRegistry} from '../function-registry'; 21 | import {ImporterRegistry} from '../importer-registry'; 22 | import {MessageTransformer} from '../message-transformer'; 23 | import {PacketTransformer} from '../packet-transformer'; 24 | import * as utils from '../utils'; 25 | import * as proto from '../vendor/embedded_sass_pb'; 26 | import {CompileResult} from '../vendor/sass/compile'; 27 | import {Options} from '../vendor/sass/options'; 28 | 29 | /** 30 | * Flag allowing the constructor passed by `initCompiler` so we can 31 | * differentiate and throw an error if the `Compiler` is constructed via `new 32 | * Compiler`. 33 | */ 34 | const initFlag = Symbol(); 35 | 36 | /** A synchronous wrapper for the embedded Sass compiler */ 37 | export class Compiler { 38 | /** The underlying process that's being wrapped. */ 39 | private readonly process = new SyncChildProcess( 40 | compilerCommand[0], 41 | [...compilerCommand.slice(1), '--embedded'], 42 | { 43 | // Use the command's cwd so the compiler survives the removal of the 44 | // current working directory. 45 | // https://github.com/sass/embedded-host-node/pull/261#discussion_r1438712923 46 | cwd: path.dirname(compilerCommand[0]), 47 | // Node blocks launching .bat and .cmd without a shell due to CVE-2024-27980 48 | shell: ['.bat', '.cmd'].includes( 49 | path.extname(compilerCommand[0]).toLowerCase(), 50 | ), 51 | windowsHide: true, 52 | }, 53 | ); 54 | 55 | /** The next compilation ID. */ 56 | private compilationId = 1; 57 | 58 | /** A list of active dispatchers. */ 59 | private readonly dispatchers: Set> = new Set(); 60 | 61 | /** The buffers emitted by the child process's stdout. */ 62 | private readonly stdout$ = new Subject(); 63 | 64 | /** The buffers emitted by the child process's stderr. */ 65 | private readonly stderr$ = new Subject(); 66 | 67 | /** Whether the underlying compiler has already exited. */ 68 | private disposed = false; 69 | 70 | /** Reusable message transformer for all compilations. */ 71 | private readonly messageTransformer: MessageTransformer; 72 | 73 | /** Writes `buffer` to the child process's stdin. */ 74 | private writeStdin(buffer: Buffer): void { 75 | this.process.stdin.write(buffer); 76 | } 77 | 78 | /** Yields the next event from the underlying process. */ 79 | private yield(): boolean { 80 | const result = this.process.next(); 81 | if (result.done) { 82 | this.disposed = true; 83 | return false; 84 | } 85 | const event = result.value; 86 | switch (event.type) { 87 | case 'stdout': 88 | this.stdout$.next(event.data); 89 | return true; 90 | 91 | case 'stderr': 92 | this.stderr$.next(event.data); 93 | return true; 94 | } 95 | } 96 | 97 | /** Blocks until the underlying process exits. */ 98 | private yieldUntilExit(): void { 99 | while (!this.disposed) { 100 | this.yield(); 101 | } 102 | } 103 | 104 | /** 105 | * Sends a compile request to the child process and returns the CompileResult. 106 | * Throws if there were any protocol or compilation errors. 107 | */ 108 | private compileRequestSync( 109 | request: proto.InboundMessage_CompileRequest, 110 | importers: ImporterRegistry<'sync'>, 111 | options?: OptionsWithLegacy<'sync'>, 112 | ): CompileResult { 113 | const optionsKey = Symbol(); 114 | activeDeprecationOptions.set(optionsKey, options ?? {}); 115 | try { 116 | const functions = new FunctionRegistry(options?.functions); 117 | 118 | const dispatcher = createDispatcher<'sync'>( 119 | this.compilationId++, 120 | this.messageTransformer, 121 | { 122 | handleImportRequest: request => importers.import(request), 123 | handleFileImportRequest: request => importers.fileImport(request), 124 | handleCanonicalizeRequest: request => importers.canonicalize(request), 125 | handleFunctionCallRequest: request => functions.call(request), 126 | }, 127 | ); 128 | this.dispatchers.add(dispatcher); 129 | 130 | dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); 131 | 132 | let error: unknown; 133 | let response: proto.OutboundMessage_CompileResponse | undefined; 134 | dispatcher.sendCompileRequest(request, (error_, response_) => { 135 | this.dispatchers.delete(dispatcher); 136 | // Reset the compilation ID when the compiler goes idle (no active 137 | // dispatchers) to avoid overflowing it. 138 | // https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794 139 | if (this.dispatchers.size === 0) this.compilationId = 1; 140 | if (error_) { 141 | error = error_; 142 | } else { 143 | response = response_; 144 | } 145 | }); 146 | 147 | for (;;) { 148 | if (!this.yield()) { 149 | throw utils.compilerError('Embedded compiler exited unexpectedly.'); 150 | } 151 | 152 | if (error) throw error; 153 | if (response) return handleCompileResponse(response); 154 | } 155 | } finally { 156 | activeDeprecationOptions.delete(optionsKey); 157 | } 158 | } 159 | 160 | /** Guards against using a disposed compiler. */ 161 | private throwIfDisposed(): void { 162 | if (this.disposed) { 163 | throw utils.compilerError('Sync compiler has already been disposed.'); 164 | } 165 | } 166 | 167 | /** Initialize resources shared across compilations. */ 168 | constructor(flag: Symbol | undefined) { 169 | if (flag !== initFlag) { 170 | throw utils.compilerError( 171 | 'Compiler can not be directly constructed. ' + 172 | 'Please use `sass.initAsyncCompiler()` instead.', 173 | ); 174 | } 175 | this.stderr$.subscribe(data => process.stderr.write(data)); 176 | const packetTransformer = new PacketTransformer(this.stdout$, buffer => { 177 | this.writeStdin(buffer); 178 | }); 179 | this.messageTransformer = new MessageTransformer( 180 | packetTransformer.outboundProtobufs$, 181 | packet => packetTransformer.writeInboundProtobuf(packet), 182 | ); 183 | } 184 | 185 | compile(path: string, options?: Options<'sync'>): CompileResult { 186 | this.throwIfDisposed(); 187 | const importers = new ImporterRegistry(options); 188 | return this.compileRequestSync( 189 | newCompilePathRequest(path, importers, options), 190 | importers, 191 | options, 192 | ); 193 | } 194 | 195 | compileString(source: string, options?: Options<'sync'>): CompileResult { 196 | this.throwIfDisposed(); 197 | const importers = new ImporterRegistry(options); 198 | return this.compileRequestSync( 199 | newCompileStringRequest(source, importers, options), 200 | importers, 201 | options, 202 | ); 203 | } 204 | 205 | dispose(): void { 206 | this.process.stdin.end(); 207 | this.yieldUntilExit(); 208 | } 209 | } 210 | 211 | export function initCompiler(): Compiler { 212 | return new Compiler(initFlag); 213 | } 214 | -------------------------------------------------------------------------------- /lib/src/compiler/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as p from 'path'; 6 | import * as supportsColor from 'supports-color'; 7 | import {create} from '@bufbuild/protobuf'; 8 | 9 | import {Deprecation, deprecations, getDeprecationIds} from '../deprecations'; 10 | import {deprotofySourceSpan} from '../deprotofy-span'; 11 | import {Dispatcher, DispatcherHandlers} from '../dispatcher'; 12 | import {Exception} from '../exception'; 13 | import {ImporterRegistry} from '../importer-registry'; 14 | import { 15 | legacyImporterProtocol, 16 | removeLegacyImporter, 17 | removeLegacyImporterFromSpan, 18 | } from '../legacy/utils'; 19 | import {Logger} from '../logger'; 20 | import {MessageTransformer} from '../message-transformer'; 21 | import * as utils from '../utils'; 22 | import * as proto from '../vendor/embedded_sass_pb'; 23 | import {SourceSpan} from '../vendor/sass'; 24 | import {CompileResult} from '../vendor/sass/compile'; 25 | import {Options, StringOptions} from '../vendor/sass/options'; 26 | 27 | /** 28 | * Allow the legacy API to pass in an option signaling to the modern API that 29 | * it's being run in legacy mode. 30 | * 31 | * This is not intended for API users to pass in, and may be broken without 32 | * warning in the future. 33 | */ 34 | export type OptionsWithLegacy = Options & { 35 | legacy?: boolean; 36 | }; 37 | 38 | /** 39 | * Allow the legacy API to pass in an option signaling to the modern API that 40 | * it's being run in legacy mode. 41 | * 42 | * This is not intended for API users to pass in, and may be broken without 43 | * warning in the future. 44 | */ 45 | export type StringOptionsWithLegacy = 46 | StringOptions & {legacy?: boolean}; 47 | 48 | /** 49 | * Creates a dispatcher that dispatches messages from the given `stdout` stream. 50 | */ 51 | export function createDispatcher( 52 | compilationId: number, 53 | messageTransformer: MessageTransformer, 54 | handlers: DispatcherHandlers, 55 | ): Dispatcher { 56 | return new Dispatcher( 57 | compilationId, 58 | messageTransformer.outboundMessages$, 59 | message => messageTransformer.writeInboundMessage(message), 60 | handlers, 61 | ); 62 | } 63 | 64 | // Creates a compilation request for the given `options` without adding any 65 | // input-specific options. 66 | function newCompileRequest( 67 | importers: ImporterRegistry<'sync' | 'async'>, 68 | options?: Options<'sync' | 'async'>, 69 | ): proto.InboundMessage_CompileRequest { 70 | const request = create(proto.InboundMessage_CompileRequestSchema, { 71 | importers: importers.importers, 72 | globalFunctions: Object.keys(options?.functions ?? {}), 73 | sourceMap: !!options?.sourceMap, 74 | sourceMapIncludeSources: !!options?.sourceMapIncludeSources, 75 | alertColor: options?.alertColor ?? !!supportsColor.stdout, 76 | alertAscii: !!options?.alertAscii, 77 | quietDeps: !!options?.quietDeps, 78 | verbose: !!options?.verbose, 79 | charset: !!(options?.charset ?? true), 80 | silent: options?.logger === Logger.silent, 81 | fatalDeprecation: getDeprecationIds(options?.fatalDeprecations ?? []), 82 | silenceDeprecation: getDeprecationIds(options?.silenceDeprecations ?? []), 83 | futureDeprecation: getDeprecationIds(options?.futureDeprecations ?? []), 84 | }); 85 | 86 | switch (options?.style ?? 'expanded') { 87 | case 'expanded': 88 | request.style = proto.OutputStyle.EXPANDED; 89 | break; 90 | 91 | case 'compressed': 92 | request.style = proto.OutputStyle.COMPRESSED; 93 | break; 94 | 95 | default: 96 | throw new Error(`Unknown options.style: "${options?.style}"`); 97 | } 98 | 99 | return request; 100 | } 101 | 102 | // Creates a request for compiling a file. 103 | export function newCompilePathRequest( 104 | path: string, 105 | importers: ImporterRegistry<'sync' | 'async'>, 106 | options?: Options<'sync' | 'async'>, 107 | ): proto.InboundMessage_CompileRequest { 108 | const absPath = p.resolve(path); 109 | const request = newCompileRequest(importers, options); 110 | request.input = {case: 'path', value: absPath}; 111 | return request; 112 | } 113 | 114 | // Creates a request for compiling a string. 115 | export function newCompileStringRequest( 116 | source: string, 117 | importers: ImporterRegistry<'sync' | 'async'>, 118 | options?: StringOptions<'sync' | 'async'>, 119 | ): proto.InboundMessage_CompileRequest { 120 | const input = create(proto.InboundMessage_CompileRequest_StringInputSchema, { 121 | source, 122 | syntax: utils.protofySyntax(options?.syntax ?? 'scss'), 123 | }); 124 | 125 | const url = options?.url?.toString(); 126 | if (url && url !== legacyImporterProtocol) { 127 | input.url = url; 128 | } 129 | 130 | if (options && 'importer' in options && options.importer) { 131 | input.importer = importers.register(options.importer); 132 | } else if (url === legacyImporterProtocol) { 133 | input.importer = create( 134 | proto.InboundMessage_CompileRequest_ImporterSchema, 135 | { 136 | importer: {case: 'path', value: p.resolve('.')}, 137 | }, 138 | ); 139 | } else { 140 | // When importer is not set on the host, the compiler will set a 141 | // FileSystemImporter if `url` is set to a file: url or a NoOpImporter. 142 | } 143 | 144 | const request = newCompileRequest(importers, options); 145 | request.input = {case: 'string', value: input}; 146 | return request; 147 | } 148 | 149 | /** Type guard to check that `id` is a valid deprecation ID. */ 150 | function validDeprecationId( 151 | id: string | number | symbol | undefined, 152 | ): id is keyof typeof deprecations { 153 | return !!id && id in deprecations; 154 | } 155 | 156 | /** Handles a log event according to `options`. */ 157 | export function handleLogEvent( 158 | options: OptionsWithLegacy<'sync' | 'async'> | undefined, 159 | event: proto.OutboundMessage_LogEvent, 160 | ): void { 161 | let span = event.span ? deprotofySourceSpan(event.span) : null; 162 | if (span && options?.legacy) span = removeLegacyImporterFromSpan(span); 163 | let message = event.message; 164 | if (options?.legacy) message = removeLegacyImporter(message); 165 | let formatted = event.formatted; 166 | if (options?.legacy) formatted = removeLegacyImporter(formatted); 167 | const deprecationType = validDeprecationId(event.deprecationType) 168 | ? deprecations[event.deprecationType] 169 | : null; 170 | 171 | if (event.type === proto.LogEventType.DEBUG) { 172 | if (options?.logger?.debug) { 173 | options.logger.debug(message, { 174 | span: span!, 175 | }); 176 | } else { 177 | console.error(formatted); 178 | } 179 | } else { 180 | if (options?.logger?.warn) { 181 | const params: ( 182 | | { 183 | deprecation: true; 184 | deprecationType: Deprecation; 185 | } 186 | | {deprecation: false} 187 | ) & { 188 | span?: SourceSpan; 189 | stack?: string; 190 | } = deprecationType 191 | ? {deprecation: true, deprecationType: deprecationType} 192 | : {deprecation: false}; 193 | if (span) params.span = span; 194 | 195 | const stack = event.stackTrace; 196 | if (stack) { 197 | params.stack = options?.legacy ? removeLegacyImporter(stack) : stack; 198 | } 199 | 200 | options.logger.warn(message, params); 201 | } else { 202 | console.error(formatted); 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * Converts a `CompileResponse` into a `CompileResult`. 209 | * 210 | * Throws a `SassException` if the compilation failed. 211 | */ 212 | export function handleCompileResponse( 213 | response: proto.OutboundMessage_CompileResponse, 214 | ): CompileResult { 215 | if (response.result.case === 'success') { 216 | const success = response.result.value; 217 | const result: CompileResult = { 218 | css: success.css, 219 | loadedUrls: response.loadedUrls.map(url => new URL(url)), 220 | }; 221 | 222 | const sourceMap = success.sourceMap; 223 | if (sourceMap) result.sourceMap = JSON.parse(sourceMap); 224 | return result; 225 | } else if (response.result.case === 'failure') { 226 | throw new Exception(response.result.value); 227 | } else { 228 | throw utils.compilerError('Compiler sent empty CompileResponse.'); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/src/deprecations.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Deprecation, DeprecationOrId, Options} from './vendor/sass'; 6 | import {Version} from './version'; 7 | 8 | export {deprecations} from './vendor/deprecations'; 9 | export {Deprecation, DeprecationOrId, DeprecationStatus} from './vendor/sass'; 10 | 11 | /** 12 | * Converts a mixed array of deprecations, IDs, and versions to an array of IDs 13 | * that's ready to include in a CompileRequest. 14 | */ 15 | export function getDeprecationIds( 16 | arr: (DeprecationOrId | Version)[], 17 | ): string[] { 18 | return arr.map(item => { 19 | if (item instanceof Version) { 20 | return item.toString(); 21 | } else if (typeof item === 'string') { 22 | return item; 23 | } 24 | return item.id; 25 | }); 26 | } 27 | 28 | /** 29 | * Map between active compilations and the deprecation options they use. 30 | * 31 | * This is used to determine which options to use when handling host-side 32 | * deprecation warnings that aren't explicitly tied to a particular compilation. 33 | */ 34 | export const activeDeprecationOptions: Map = 35 | new Map(); 36 | 37 | /** 38 | * Shorthand for the subset of options related to deprecations. 39 | */ 40 | export type DeprecationOptions = Pick< 41 | Options<'sync'>, 42 | 'fatalDeprecations' | 'futureDeprecations' | 'silenceDeprecations' 43 | >; 44 | 45 | /** 46 | * Handles a host-side deprecation warning, either emitting a warning, throwing 47 | * an error, or doing nothing depending on the deprecation options used. 48 | * 49 | * If no specific deprecation options are passed here, then options will be 50 | * determined based on the options of the active compilations. 51 | */ 52 | export function warnForHostSideDeprecation( 53 | message: string, 54 | deprecation: Deprecation, 55 | options?: DeprecationOptions, 56 | ): void { 57 | if ( 58 | deprecation.status === 'future' && 59 | !isEnabledFuture(deprecation, options) 60 | ) { 61 | return; 62 | } 63 | const fullMessage = `Deprecation [${deprecation.id}]: ${message}`; 64 | if (isFatal(deprecation, options)) { 65 | throw Error(fullMessage); 66 | } 67 | if (!isSilent(deprecation, options)) { 68 | console.warn(fullMessage); 69 | } 70 | } 71 | 72 | /** 73 | * Checks whether the given deprecation is included in the given list of silent 74 | * deprecations or is silenced by at least one active compilation. 75 | */ 76 | function isSilent( 77 | deprecation: Deprecation, 78 | options?: DeprecationOptions, 79 | ): boolean { 80 | if (!options) { 81 | for (const potentialOptions of activeDeprecationOptions.values()) { 82 | if (isSilent(deprecation, potentialOptions)) return true; 83 | } 84 | return false; 85 | } 86 | return getDeprecationIds(options.silenceDeprecations ?? []).includes( 87 | deprecation.id, 88 | ); 89 | } 90 | 91 | /** 92 | * Checks whether the given deprecation is included in the given list of future 93 | * deprecations that should be enabled or is enabled in all active compilations. 94 | */ 95 | function isEnabledFuture( 96 | deprecation: Deprecation, 97 | options?: DeprecationOptions, 98 | ): boolean { 99 | if (!options) { 100 | for (const potentialOptions of activeDeprecationOptions.values()) { 101 | if (!isEnabledFuture(deprecation, potentialOptions)) return false; 102 | } 103 | return activeDeprecationOptions.size > 0; 104 | } 105 | return getDeprecationIds(options.futureDeprecations ?? []).includes( 106 | deprecation.id, 107 | ); 108 | } 109 | 110 | /** 111 | * Checks whether the given deprecation is included in the given list of 112 | * fatal deprecations or is marked as fatal in all active compilations. 113 | */ 114 | function isFatal( 115 | deprecation: Deprecation, 116 | options?: DeprecationOptions, 117 | ): boolean { 118 | if (!options) { 119 | for (const potentialOptions of activeDeprecationOptions.values()) { 120 | if (!isFatal(deprecation, potentialOptions)) return false; 121 | } 122 | return activeDeprecationOptions.size > 0; 123 | } 124 | const versionNumber = 125 | deprecation.deprecatedIn === null 126 | ? null 127 | : deprecation.deprecatedIn.major * 1000000 + 128 | deprecation.deprecatedIn.minor * 1000 + 129 | deprecation.deprecatedIn.patch; 130 | for (const fatal of options.fatalDeprecations ?? []) { 131 | if (fatal instanceof Version) { 132 | if (versionNumber === null) continue; 133 | if ( 134 | versionNumber <= 135 | fatal.major * 1000000 + fatal.minor * 1000 + fatal.patch 136 | ) { 137 | return true; 138 | } 139 | } else if (typeof fatal === 'string') { 140 | if (fatal === deprecation.id) return true; 141 | } else { 142 | if ((fatal as Deprecation).id === deprecation.id) return true; 143 | } 144 | } 145 | return false; 146 | } 147 | -------------------------------------------------------------------------------- /lib/src/deprotofy-span.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {URL} from 'url'; 6 | 7 | import * as proto from './vendor/embedded_sass_pb'; 8 | import {SourceSpan} from './vendor/sass'; 9 | import {compilerError} from './utils'; 10 | 11 | // Creates a SourceSpan from the given protocol `buffer`. Throws if the buffer 12 | // has invalid fields. 13 | export function deprotofySourceSpan(buffer: proto.SourceSpan): SourceSpan { 14 | const text = buffer.text; 15 | 16 | if (buffer.start === undefined) { 17 | throw compilerError('Expected SourceSpan to have start.'); 18 | } 19 | 20 | let end; 21 | if (buffer.end === undefined) { 22 | if (text !== '') { 23 | throw compilerError('Expected SourceSpan text to be empty.'); 24 | } else { 25 | end = buffer.start; 26 | } 27 | } else { 28 | end = buffer.end; 29 | if (end.offset < buffer.start.offset) { 30 | throw compilerError('Expected SourceSpan end to be after start.'); 31 | } 32 | } 33 | 34 | const url = buffer.url === '' ? undefined : new URL(buffer.url); 35 | 36 | const context = buffer.context === '' ? undefined : buffer.context; 37 | 38 | return { 39 | text, 40 | start: buffer.start, 41 | end, 42 | url, 43 | context, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/elf.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as fs from 'fs'; 6 | 7 | /** Read a chunk of data from a file descriptor into a new Buffer. */ 8 | function readFileDescriptor( 9 | fd: number, 10 | position: number, 11 | length: number, 12 | ): Buffer { 13 | const buffer = Buffer.alloc(length); 14 | let offset = 0; 15 | while (offset < length) { 16 | const bytesRead = fs.readSync(fd, buffer, { 17 | offset: offset, 18 | position: position + offset, 19 | }); 20 | if (bytesRead === 0) { 21 | throw new Error(`failed to read fd ${fd}`); 22 | } 23 | 24 | offset += bytesRead; 25 | } 26 | return buffer; 27 | } 28 | 29 | /** Parse an ELF file and return its interpreter. */ 30 | export function getElfInterpreter(path: string): string { 31 | const fd = fs.openSync(path, 'r'); 32 | try { 33 | const elfIdentification = new DataView( 34 | readFileDescriptor(fd, 0, 64).buffer, 35 | ); 36 | 37 | if ( 38 | elfIdentification.getUint8(0) !== 0x7f || 39 | elfIdentification.getUint8(1) !== 0x45 || 40 | elfIdentification.getUint8(2) !== 0x4c || 41 | elfIdentification.getUint8(3) !== 0x46 42 | ) { 43 | throw new Error(`${path} is not an ELF file.`); 44 | } 45 | 46 | const elfIdentificationClass = elfIdentification.getUint8(4); 47 | if (elfIdentificationClass !== 1 && elfIdentificationClass !== 2) { 48 | throw new Error(`${path} has an invalid ELF class.`); 49 | } 50 | const elfClass32 = elfIdentificationClass === 1; 51 | 52 | const elfIdentificationData = elfIdentification.getUint8(5); 53 | if (elfIdentificationData !== 1 && elfIdentificationData !== 2) { 54 | throw new Error(`${path} has an invalid endianness.`); 55 | } 56 | const littleEndian = elfIdentificationData === 1; 57 | 58 | // Converting BigUint64 to Number because node Buffer length has to be 59 | // number type, and we don't expect any elf we check with this method to 60 | // be larger than 9007199254740991 bytes. 61 | const programHeadersOffset = elfClass32 62 | ? elfIdentification.getUint32(28, littleEndian) 63 | : Number(elfIdentification.getBigUint64(32, littleEndian)); 64 | const programHeadersEntrySize = elfClass32 65 | ? elfIdentification.getUint16(42, littleEndian) 66 | : elfIdentification.getUint16(54, littleEndian); 67 | const programHeadersEntryCount = elfClass32 68 | ? elfIdentification.getUint16(44, littleEndian) 69 | : elfIdentification.getUint16(56, littleEndian); 70 | 71 | const programHeaders = new DataView( 72 | readFileDescriptor( 73 | fd, 74 | programHeadersOffset, 75 | programHeadersEntrySize * programHeadersEntryCount, 76 | ).buffer, 77 | ); 78 | for (let i = 0; i < programHeadersEntryCount; i++) { 79 | const byteOffset = i * programHeadersEntrySize; 80 | const segmentType = programHeaders.getUint32(byteOffset, littleEndian); 81 | if (segmentType !== 3) continue; // 3 is PT_INTERP, the interpreter 82 | 83 | const segmentOffset = elfClass32 84 | ? programHeaders.getUint32(byteOffset + 4, littleEndian) 85 | : Number(programHeaders.getBigUint64(byteOffset + 8, littleEndian)); 86 | const segmentFileSize = elfClass32 87 | ? programHeaders.getUint32(byteOffset + 16, littleEndian) 88 | : Number(programHeaders.getBigUint64(byteOffset + 32, littleEndian)); 89 | 90 | const buffer = readFileDescriptor(fd, segmentOffset, segmentFileSize); 91 | if (buffer[segmentFileSize - 1] !== 0) { 92 | throw new Error(`${path} is corrupted.`); 93 | } 94 | 95 | return buffer.toString('utf8', 0, segmentFileSize - 1); 96 | } 97 | 98 | throw new Error(`${path} does not contain an interpreter entry.`); 99 | } finally { 100 | fs.closeSync(fd); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/exception.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as proto from './vendor/embedded_sass_pb'; 6 | import {Exception as SassException, SourceSpan} from './vendor/sass'; 7 | import {deprotofySourceSpan} from './deprotofy-span'; 8 | 9 | export class Exception extends Error implements SassException { 10 | readonly sassMessage: string; 11 | readonly sassStack: string; 12 | readonly span: SourceSpan; 13 | 14 | constructor(failure: proto.OutboundMessage_CompileResponse_CompileFailure) { 15 | super(failure.formatted); 16 | 17 | this.sassMessage = failure.message; 18 | this.sassStack = failure.stackTrace; 19 | this.span = deprotofySourceSpan(failure.span!); 20 | } 21 | 22 | toString(): string { 23 | return this.message; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/function-registry.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {inspect} from 'util'; 6 | import {create} from '@bufbuild/protobuf'; 7 | 8 | import * as types from './vendor/sass'; 9 | import * as utils from './utils'; 10 | import {CustomFunction} from './vendor/sass'; 11 | import * as proto from './vendor/embedded_sass_pb'; 12 | import {PromiseOr, catchOr, compilerError, thenOr} from './utils'; 13 | import {Protofier} from './protofier'; 14 | import {Value} from './value'; 15 | 16 | /** 17 | * Tracks functions that are defined on the host so that the compiler can 18 | * execute them. 19 | */ 20 | export class FunctionRegistry { 21 | /** 22 | * The globally unique identifier of the current compilation used for tracking 23 | * the ownership of CompilerFunction and CompilerMixin objects. 24 | */ 25 | public readonly compileContext = Symbol(); 26 | private readonly functionsByName = new Map>(); 27 | private readonly functionsById = new Map>(); 28 | private readonly idsByFunction = new Map, number>(); 29 | 30 | /** The next ID to use for a function. */ 31 | private id = 0; 32 | 33 | constructor(functionsBySignature?: Record>) { 34 | for (const [signature, fn] of Object.entries(functionsBySignature ?? {})) { 35 | const openParen = signature.indexOf('('); 36 | if (openParen === -1) { 37 | throw new Error(`options.functions: "${signature}" is missing "("`); 38 | } 39 | 40 | this.functionsByName.set(signature.substring(0, openParen), fn); 41 | } 42 | } 43 | 44 | /** Registers `fn` as a function that can be called using the returned ID. */ 45 | register(fn: CustomFunction): number { 46 | return utils.putIfAbsent(this.idsByFunction, fn, () => { 47 | const id = this.id; 48 | this.id += 1; 49 | this.functionsById.set(id, fn); 50 | return id; 51 | }); 52 | } 53 | 54 | /** 55 | * Returns the function to which `request` refers and returns its response. 56 | */ 57 | call( 58 | request: proto.OutboundMessage_FunctionCallRequest, 59 | ): PromiseOr { 60 | const protofier = new Protofier(this); 61 | const fn = this.get(request); 62 | 63 | return catchOr( 64 | () => { 65 | return thenOr( 66 | fn( 67 | request.arguments.map( 68 | value => protofier.deprotofy(value) as types.Value, 69 | ), 70 | ), 71 | result => { 72 | if (!(result instanceof Value)) { 73 | const name = 74 | request.identifier.case === 'name' 75 | ? `"${request.identifier.value}"` 76 | : 'anonymous function'; 77 | throw ( 78 | `options.functions: ${name} returned non-Value: ` + 79 | inspect(result) 80 | ); 81 | } 82 | 83 | return create(proto.InboundMessage_FunctionCallResponseSchema, { 84 | result: {case: 'success', value: protofier.protofy(result)}, 85 | accessedArgumentLists: protofier.accessedArgumentLists, 86 | }); 87 | }, 88 | ); 89 | }, 90 | error => 91 | create(proto.InboundMessage_FunctionCallResponseSchema, { 92 | result: {case: 'error', value: `${error}`}, 93 | }), 94 | ); 95 | } 96 | 97 | /** Returns the function to which `request` refers. */ 98 | private get( 99 | request: proto.OutboundMessage_FunctionCallRequest, 100 | ): CustomFunction { 101 | if (request.identifier.case === 'name') { 102 | const fn = this.functionsByName.get(request.identifier.value); 103 | if (fn) return fn; 104 | 105 | throw compilerError( 106 | 'Invalid OutboundMessage_FunctionCallRequest: there is no function ' + 107 | `named "${request.identifier.value}"`, 108 | ); 109 | } else if (request.identifier.case === 'functionId') { 110 | const fn = this.functionsById.get(request.identifier.value); 111 | if (fn) return fn; 112 | 113 | throw compilerError( 114 | 'Invalid OutboundMessage_FunctionCallRequest: there is no function ' + 115 | `with ID "${request.identifier.value}"`, 116 | ); 117 | } else { 118 | throw compilerError( 119 | 'Invalid OutboundMessage_FunctionCallRequest: function identifier is ' + 120 | 'unset', 121 | ); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/src/legacy/resolve-path.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as fs from 'fs'; 6 | import * as p from 'path'; 7 | 8 | /** 9 | * Resolves a path using the same logic as the filesystem importer. 10 | * 11 | * This tries to fill in extensions and partial prefixes and check for a 12 | * directory default. If no file can be found, it returns `null`. 13 | */ 14 | export function resolvePath(path: string, fromImport: boolean): string | null { 15 | const extension = p.extname(path); 16 | if (extension === '.sass' || extension === '.scss' || extension === '.css') { 17 | return ( 18 | (fromImport 19 | ? exactlyOne(tryPath(`${withoutExtension(path)}.import${extension}`)) 20 | : null) ?? exactlyOne(tryPath(path)) 21 | ); 22 | } 23 | 24 | return ( 25 | (fromImport ? exactlyOne(tryPathWithExtensions(`${path}.import`)) : null) ?? 26 | exactlyOne(tryPathWithExtensions(path)) ?? 27 | tryPathAsDirectory(path, fromImport) 28 | ); 29 | } 30 | 31 | // Like `tryPath`, but checks `.sass`, `.scss`, and `.css` extensions. 32 | function tryPathWithExtensions(path: string): string[] { 33 | const result = [...tryPath(path + '.sass'), ...tryPath(path + '.scss')]; 34 | return result.length > 0 ? result : tryPath(path + '.css'); 35 | } 36 | 37 | // Returns the `path` and/or the partial with the same name, if either or both 38 | // exists. If neither exists, returns an empty list. 39 | function tryPath(path: string): string[] { 40 | const partial = p.join(p.dirname(path), `_${p.basename(path)}`); 41 | const result: string[] = []; 42 | if (fileExists(partial)) result.push(partial); 43 | if (fileExists(path)) result.push(path); 44 | return result; 45 | } 46 | 47 | // Returns the resolved index file for `path` if `path` is a directory and the 48 | // index file exists. Otherwise, returns `null`. 49 | function tryPathAsDirectory(path: string, fromImport: boolean): string | null { 50 | if (!dirExists(path)) return null; 51 | 52 | return ( 53 | (fromImport 54 | ? exactlyOne(tryPathWithExtensions(p.join(path, 'index.import'))) 55 | : null) ?? exactlyOne(tryPathWithExtensions(p.join(path, 'index'))) 56 | ); 57 | } 58 | 59 | // If `paths` contains exactly one path, returns that path. If it contains no 60 | // paths, returns `null`. If it contains more than one, throws an exception. 61 | function exactlyOne(paths: string[]): string | null { 62 | if (paths.length === 0) return null; 63 | if (paths.length === 1) return paths[0]; 64 | 65 | throw new Error( 66 | "It's not clear which file to import. Found:\n" + 67 | paths.map(path => ' ' + path).join('\n'), 68 | ); 69 | } 70 | 71 | // Returns whether or not a file (not a directory) exists at `path`. 72 | function fileExists(path: string): boolean { 73 | // `existsSync()` is faster than `statSync()`, but it doesn't clarify whether 74 | // the entity in question is a file or a directory. Since false negatives are 75 | // much more common than false positives, it works out in our favor to check 76 | // this first. 77 | if (!fs.existsSync(path)) return false; 78 | 79 | try { 80 | return fs.statSync(path).isFile(); 81 | } catch (error: unknown) { 82 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; 83 | throw error; 84 | } 85 | } 86 | 87 | // Returns whether or not a directory (not a file) exists at `path`. 88 | function dirExists(path: string): boolean { 89 | // `existsSync()` is faster than `statSync()`, but it doesn't clarify whether 90 | // the entity in question is a file or a directory. Since false negatives are 91 | // much more common than false positives, it works out in our favor to check 92 | // this first. 93 | if (!fs.existsSync(path)) return false; 94 | 95 | try { 96 | return fs.statSync(path).isDirectory(); 97 | } catch (error: unknown) { 98 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; 99 | throw error; 100 | } 101 | } 102 | 103 | // Returns `path` without its file extension. 104 | function withoutExtension(path: string): string { 105 | const extension = p.extname(path); 106 | return path.substring(0, path.length - extension.length); 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/legacy/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {strict as assert} from 'assert'; 6 | import {pathToFileURL} from 'url'; 7 | 8 | import {fileUrlToPathCrossPlatform} from '../utils'; 9 | import {SourceSpan} from '../vendor/sass'; 10 | import {legacyImporterFileProtocol} from './importer'; 11 | 12 | /** 13 | * The URL protocol to use for URLs canonicalized using `LegacyImporterWrapper`. 14 | */ 15 | export const legacyImporterProtocol = 'legacy-importer:'; 16 | 17 | /** 18 | * The prefix for absolute URLs canonicalized using `LegacyImporterWrapper`. 19 | * 20 | * This is used to distinguish imports resolved relative to URLs returned by a 21 | * legacy importer from manually-specified absolute URLs. 22 | */ 23 | export const legacyImporterProtocolPrefix = 'legacy-importer-'; 24 | 25 | // A regular expression that matches legacy importer protocol syntax that 26 | // should be removed from human-readable messages. 27 | const removeLegacyImporterRegExp = new RegExp( 28 | `${legacyImporterProtocol}|${legacyImporterProtocolPrefix}`, 29 | 'g', 30 | ); 31 | 32 | // Returns `string` with all instances of legacy importer syntax removed. 33 | export function removeLegacyImporter(string: string): string { 34 | return string.replace(removeLegacyImporterRegExp, ''); 35 | } 36 | 37 | // Returns a copy of [span] with the URL updated to remove legacy importer 38 | // syntax. 39 | export function removeLegacyImporterFromSpan(span: SourceSpan): SourceSpan { 40 | if (!span.url) return span; 41 | return { 42 | ...span, 43 | url: new URL( 44 | removeLegacyImporter(span.url.toString()), 45 | pathToFileURL(process.cwd()), 46 | ), 47 | }; 48 | } 49 | 50 | // Converts [path] to a `file:` URL and adds the [legacyImporterProtocolPrefix] 51 | // to the beginning so we can distinguish it from manually-specified absolute 52 | // `file:` URLs. 53 | export function pathToLegacyFileUrl(path: string): URL { 54 | return new URL(`${legacyImporterProtocolPrefix}${pathToFileURL(path)}`); 55 | } 56 | 57 | // Converts a `file:` URL with [legacyImporterProtocolPrefix] to the filesystem 58 | // path which it represents. 59 | export function legacyFileUrlToPath(url: URL): string { 60 | assert.equal(url.protocol, legacyImporterFileProtocol); 61 | const originalUrl = url 62 | .toString() 63 | .substring(legacyImporterProtocolPrefix.length); 64 | return fileUrlToPathCrossPlatform(originalUrl); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/legacy/value/base.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Value} from '../../value'; 6 | 7 | /** 8 | * A base class for legacy value types. A shared base class makes it easier to 9 | * detect legacy values and extract their inner value objects. 10 | */ 11 | export class LegacyValueBase { 12 | constructor(public inner: T) {} 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/legacy/value/color.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {isNullOrUndefined} from '../../utils'; 6 | import {SassColor} from '../../value/color'; 7 | import {LegacyValueBase} from './base'; 8 | 9 | export class LegacyColor extends LegacyValueBase { 10 | constructor(red: number, green: number, blue: number, alpha?: number); 11 | constructor(argb: number); 12 | constructor(inner: SassColor); 13 | 14 | constructor( 15 | redOrArgb: number | SassColor, 16 | green?: number, 17 | blue?: number, 18 | alpha?: number, 19 | ) { 20 | if (redOrArgb instanceof SassColor) { 21 | super(redOrArgb); 22 | return; 23 | } 24 | 25 | let red: number; 26 | if (isNullOrUndefined(green) || isNullOrUndefined(blue)) { 27 | const argb = redOrArgb as number; 28 | alpha = (argb >> 24) / 0xff; 29 | red = (argb >> 16) % 0x100; 30 | green = (argb >> 8) % 0x100; 31 | blue = argb % 0x100; 32 | } else { 33 | red = redOrArgb!; 34 | } 35 | 36 | super( 37 | new SassColor({ 38 | red: clamp(red, 0, 255), 39 | green: clamp(green as number, 0, 255), 40 | blue: clamp(blue as number, 0, 255), 41 | alpha: alpha ? clamp(alpha, 0, 1) : 1, 42 | }), 43 | ); 44 | } 45 | 46 | getR(): number { 47 | return this.inner.red; 48 | } 49 | 50 | setR(value: number): void { 51 | this.inner = this.inner.change({red: clamp(value, 0, 255)}); 52 | } 53 | 54 | getG(): number { 55 | return this.inner.green; 56 | } 57 | 58 | setG(value: number): void { 59 | this.inner = this.inner.change({green: clamp(value, 0, 255)}); 60 | } 61 | 62 | getB(): number { 63 | return this.inner.blue; 64 | } 65 | 66 | setB(value: number): void { 67 | this.inner = this.inner.change({blue: clamp(value, 0, 255)}); 68 | } 69 | 70 | getA(): number { 71 | return this.inner.alpha; 72 | } 73 | 74 | setA(value: number): void { 75 | this.inner = this.inner.change({alpha: clamp(value, 0, 1)}); 76 | } 77 | } 78 | 79 | Object.defineProperty(LegacyColor, 'name', {value: 'sass.types.Color'}); 80 | 81 | // Returns `number` clamped to between `min` and `max`. 82 | function clamp(num: number, min: number, max: number): number { 83 | return Math.min(Math.max(num, min), max); 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/legacy/value/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {SassBooleanInternal} from '../../value/boolean'; 6 | import {SassNull} from '../../value/null'; 7 | import {LegacyColor} from './color'; 8 | import {LegacyList} from './list'; 9 | import {LegacyMap} from './map'; 10 | import {LegacyNumber} from './number'; 11 | import {LegacyString} from './string'; 12 | 13 | export const Boolean = SassBooleanInternal; 14 | export const Color = LegacyColor; 15 | export const List = LegacyList; 16 | export const Map = LegacyMap; 17 | export const Null = SassNull; 18 | export const Number = LegacyNumber; 19 | export const String = LegacyString; 20 | 21 | // For the `sass.types.Error` object, we just re-export the native Error class. 22 | export const Error = global.Error; 23 | -------------------------------------------------------------------------------- /lib/src/legacy/value/list.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {LegacyValueBase} from './base'; 6 | import {LegacyValue} from '../../vendor/sass'; 7 | import {SassList} from '../../value/list'; 8 | import {sassNull} from '../../value/null'; 9 | import {unwrapValue, wrapValue} from './wrap'; 10 | 11 | export class LegacyList extends LegacyValueBase { 12 | constructor(length: number, commaSeparator?: boolean); 13 | constructor(inner: SassList); 14 | 15 | constructor(lengthOrInner: number | SassList, commaSeparator?: boolean) { 16 | if (lengthOrInner instanceof SassList) { 17 | super(lengthOrInner); 18 | return; 19 | } 20 | 21 | super( 22 | new SassList(new Array(lengthOrInner).fill(sassNull), { 23 | separator: commaSeparator === false ? ' ' : ',', 24 | }), 25 | ); 26 | } 27 | 28 | getValue(index: number): LegacyValue | undefined { 29 | const length = this.inner.asList.size; 30 | if (index < 0 || index >= length) { 31 | throw new Error( 32 | `Invalid index ${index}, must be between 0 and ${length}`, 33 | ); 34 | } 35 | const value = this.inner.get(index); 36 | return value ? wrapValue(value) : undefined; 37 | } 38 | 39 | setValue(index: number, value: LegacyValue): void { 40 | this.inner = new SassList( 41 | this.inner.asList.set(index, unwrapValue(value)), 42 | { 43 | separator: this.inner.separator, 44 | brackets: this.inner.hasBrackets, 45 | }, 46 | ); 47 | } 48 | 49 | getSeparator(): boolean { 50 | return this.inner.separator === ','; 51 | } 52 | 53 | setSeparator(isComma: boolean): void { 54 | this.inner = new SassList(this.inner.asList, { 55 | separator: isComma ? ',' : ' ', 56 | brackets: this.inner.hasBrackets, 57 | }); 58 | } 59 | 60 | getLength(): number { 61 | return this.inner.asList.size; 62 | } 63 | } 64 | 65 | Object.defineProperty(LegacyList, 'name', {value: 'sass.types.List'}); 66 | -------------------------------------------------------------------------------- /lib/src/legacy/value/map.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {OrderedMap} from 'immutable'; 6 | 7 | import {LegacyValueBase} from './base'; 8 | import {LegacyValue} from '../../vendor/sass'; 9 | import {SassMap} from '../../value/map'; 10 | import {SassNumber} from '../../value/number'; 11 | import {Value} from '../../value'; 12 | import {sassNull} from '../../value/null'; 13 | import {unwrapValue, wrapValue} from './wrap'; 14 | 15 | export class LegacyMap extends LegacyValueBase { 16 | constructor(lengthOrInner: number | SassMap) { 17 | if (lengthOrInner instanceof SassMap) { 18 | super(lengthOrInner); 19 | return; 20 | } 21 | 22 | super( 23 | new SassMap( 24 | OrderedMap( 25 | Array.from({length: lengthOrInner}, (_, i) => [ 26 | new SassNumber(i), 27 | sassNull, 28 | ]), 29 | ), 30 | ), 31 | ); 32 | } 33 | 34 | getValue(index: number): LegacyValue { 35 | const value = this.inner.contents.valueSeq().get(index); 36 | if (index < 0 || !value) { 37 | throw new Error( 38 | `Invalid index ${index}, must be between 0 and ` + 39 | this.inner.contents.size, 40 | ); 41 | } 42 | 43 | return wrapValue(value); 44 | } 45 | 46 | setValue(index: number, value: LegacyValue): void { 47 | this.inner = new SassMap( 48 | this.inner.contents.set(this.getUnwrappedKey(index), unwrapValue(value)), 49 | ); 50 | } 51 | 52 | getKey(index: number): LegacyValue { 53 | return wrapValue(this.getUnwrappedKey(index)); 54 | } 55 | 56 | // Like `getKey()`, but returns the unwrapped non-legacy value. 57 | private getUnwrappedKey(index: number): Value { 58 | const key = this.inner.contents.keySeq().get(index); 59 | if (index >= 0 && key) return key; 60 | throw new Error( 61 | `Invalid index ${index}, must be between 0 and ` + 62 | this.inner.contents.size, 63 | ); 64 | } 65 | 66 | setKey(index: number, key: LegacyValue): void { 67 | const oldMap = this.inner.contents; 68 | if (index < 0 || index >= oldMap.size) { 69 | throw new Error( 70 | `Invalid index ${index}, must be between 0 and ${oldMap.size}`, 71 | ); 72 | } 73 | 74 | const newKey = unwrapValue(key); 75 | const newMap = OrderedMap().asMutable(); 76 | 77 | let i = 0; 78 | for (const [oldKey, oldValue] of oldMap.entries()) { 79 | if (i === index) { 80 | newMap.set(newKey, oldValue); 81 | } else { 82 | if (newKey.equals(oldKey)) { 83 | throw new Error(`${key} is already in the map`); 84 | } 85 | newMap.set(oldKey, oldValue); 86 | } 87 | i++; 88 | } 89 | 90 | this.inner = new SassMap(newMap.asImmutable()); 91 | } 92 | 93 | getLength(): number { 94 | return this.inner.contents.size; 95 | } 96 | } 97 | 98 | Object.defineProperty(LegacyMap, 'name', {value: 'sass.types.Map'}); 99 | -------------------------------------------------------------------------------- /lib/src/legacy/value/number.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {SassNumber} from '../../value/number'; 6 | import {LegacyValueBase} from './base'; 7 | 8 | export class LegacyNumber extends LegacyValueBase { 9 | constructor(valueOrInner: number | SassNumber, unit?: string) { 10 | super( 11 | valueOrInner instanceof SassNumber 12 | ? valueOrInner 13 | : parseNumber(valueOrInner, unit), 14 | ); 15 | } 16 | 17 | getValue(): number { 18 | return this.inner.value; 19 | } 20 | 21 | setValue(value: number): void { 22 | this.inner = new SassNumber(value, { 23 | numeratorUnits: this.inner.numeratorUnits, 24 | denominatorUnits: this.inner.denominatorUnits, 25 | }); 26 | } 27 | 28 | getUnit(): string { 29 | return ( 30 | this.inner.numeratorUnits.join('*') + 31 | (this.inner.denominatorUnits.size === 0 ? '' : '/') + 32 | this.inner.denominatorUnits.join('*') 33 | ); 34 | } 35 | 36 | setUnit(unit: string): void { 37 | this.inner = parseNumber(this.inner.value, unit); 38 | } 39 | } 40 | 41 | Object.defineProperty(LegacyNumber, 'name', {value: 'sass.types.Number'}); 42 | 43 | // Parses a `SassNumber` from `value` and `unit`, using Node Sass's unit 44 | // format. 45 | function parseNumber(value: number, unit?: string): SassNumber { 46 | if (!unit) return new SassNumber(value); 47 | 48 | if (!unit.includes('*') && !unit.includes('/')) { 49 | return new SassNumber(value, unit); 50 | } 51 | 52 | const invalidUnit = new Error(`Unit ${unit} is invalid`); 53 | 54 | const operands = unit.split('/'); 55 | if (operands.length > 2) throw invalidUnit; 56 | 57 | const numerator = operands[0]; 58 | const denominator = operands.length === 1 ? null : operands[1]; 59 | 60 | const numeratorUnits = numerator.length === 0 ? [] : numerator.split('*'); 61 | if (numeratorUnits.some(unit => unit.length === 0)) throw invalidUnit; 62 | 63 | const denominatorUnits = denominator === null ? [] : denominator.split('*'); 64 | if (denominatorUnits.some(unit => unit.length === 0)) throw invalidUnit; 65 | 66 | return new SassNumber(value, { 67 | numeratorUnits: numeratorUnits, 68 | denominatorUnits: denominatorUnits, 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/legacy/value/string.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {SassString} from '../../value/string'; 6 | import {LegacyValueBase} from './base'; 7 | 8 | export class LegacyString extends LegacyValueBase { 9 | constructor(valueOrInner: string | SassString) { 10 | if (valueOrInner instanceof SassString) { 11 | super(valueOrInner); 12 | } else { 13 | super(new SassString(valueOrInner, {quotes: false})); 14 | } 15 | } 16 | 17 | getValue(): string { 18 | return this.inner.text; 19 | } 20 | 21 | setValue(value: string): void { 22 | this.inner = new SassString(value, {quotes: false}); 23 | } 24 | } 25 | 26 | Object.defineProperty(LegacyString, 'name', {value: 'sass.types.String'}); 27 | -------------------------------------------------------------------------------- /lib/src/legacy/value/wrap.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as util from 'util'; 6 | 7 | import {LegacyValueBase} from './base'; 8 | import {LegacyColor} from './color'; 9 | import {LegacyList} from './list'; 10 | import {LegacyMap} from './map'; 11 | import {LegacyNumber} from './number'; 12 | import {LegacyString} from './string'; 13 | import {PromiseOr, SyncBoolean} from '../../utils'; 14 | import {Value} from '../../value'; 15 | import {SassColor} from '../../value/color'; 16 | import {SassList} from '../../value/list'; 17 | import {SassMap} from '../../value/map'; 18 | import {SassNumber} from '../../value/number'; 19 | import {SassString} from '../../value/string'; 20 | import { 21 | CustomFunction, 22 | LegacyFunction, 23 | LegacyPluginThis, 24 | LegacyValue, 25 | } from '../../vendor/sass'; 26 | import * as types from '../../vendor/sass'; 27 | 28 | /** 29 | * Converts a `LegacyFunction` into a `CustomFunction` so it can be passed to 30 | * the new JS API. 31 | */ 32 | export function wrapFunction( 33 | thisArg: LegacyPluginThis, 34 | callback: LegacyFunction, 35 | sync: SyncBoolean, 36 | ): CustomFunction { 37 | if (sync) { 38 | return args => 39 | unwrapTypedValue( 40 | (callback as LegacyFunction<'sync'>).apply( 41 | thisArg, 42 | args.map(wrapValue), 43 | ), 44 | ); 45 | } else { 46 | return args => 47 | new Promise((resolve, reject) => { 48 | function done(result: unknown): void { 49 | try { 50 | if (result instanceof Error) { 51 | reject(result); 52 | } else { 53 | resolve(unwrapTypedValue(result)); 54 | } 55 | } catch (error: unknown) { 56 | reject(error); 57 | } 58 | } 59 | 60 | // The cast here is necesary to work around microsoft/TypeScript#33815. 61 | const syncResult = (callback as (...args: unknown[]) => unknown).apply( 62 | thisArg, 63 | [...args.map(wrapValue), done], 64 | ); 65 | 66 | if (syncResult !== undefined) resolve(unwrapTypedValue(syncResult)); 67 | }) as PromiseOr; 68 | } 69 | } 70 | 71 | // Like `unwrapValue()`, but returns a `types.Value` type. 72 | function unwrapTypedValue(value: unknown): types.Value { 73 | return unwrapValue(value) as types.Value; 74 | } 75 | 76 | /** Converts a value returned by a `LegacyFunction` into a `Value`. */ 77 | export function unwrapValue(value: unknown): Value { 78 | if (value instanceof Error) throw value; 79 | if (value instanceof Value) return value; 80 | if (value instanceof LegacyValueBase) return value.inner; 81 | throw new Error(`Expected legacy Sass value, got ${util.inspect(value)}.`); 82 | } 83 | 84 | /** Converts a `Value` into a `LegacyValue`. */ 85 | export function wrapValue(value: Value | types.Value): LegacyValue { 86 | if (value instanceof SassColor) return new LegacyColor(value); 87 | if (value instanceof SassList) return new LegacyList(value); 88 | if (value instanceof SassMap) return new LegacyMap(value); 89 | if (value instanceof SassNumber) return new LegacyNumber(value); 90 | if (value instanceof SassString) return new LegacyString(value); 91 | if (value instanceof Value) return value; 92 | throw new Error(`Expected Sass value, got ${util.inspect(value)}.`); 93 | } 94 | -------------------------------------------------------------------------------- /lib/src/logger.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | export const Logger = { 6 | silent: {warn() {}, debug() {}}, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/src/message-transformer.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Observable, Subject} from 'rxjs'; 6 | import * as varint from 'varint'; 7 | import {create, toBinary} from '@bufbuild/protobuf'; 8 | 9 | import {expectObservableToError} from '../../test/utils'; 10 | import {MessageTransformer} from './message-transformer'; 11 | import * as proto from './vendor/embedded_sass_pb'; 12 | 13 | describe('message transformer', () => { 14 | let messages: MessageTransformer; 15 | 16 | function validInboundMessage(source: string): proto.InboundMessage { 17 | return create(proto.InboundMessageSchema, { 18 | message: { 19 | case: 'compileRequest', 20 | value: { 21 | input: { 22 | case: 'string', 23 | value: { 24 | source, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }); 30 | } 31 | 32 | describe('encode', () => { 33 | let encodedProtobufs: Uint8Array[]; 34 | 35 | beforeEach(() => { 36 | encodedProtobufs = []; 37 | messages = new MessageTransformer(new Observable(), buffer => 38 | encodedProtobufs.push(buffer), 39 | ); 40 | }); 41 | 42 | it('encodes an InboundMessage to buffer', () => { 43 | const message = validInboundMessage('a {b: c}'); 44 | messages.writeInboundMessage([1234, message]); 45 | expect(encodedProtobufs).toEqual([ 46 | Uint8Array.from([ 47 | ...varint.encode(1234), 48 | ...toBinary(proto.InboundMessageSchema, message), 49 | ]), 50 | ]); 51 | }); 52 | }); 53 | 54 | describe('decode', () => { 55 | let protobufs$: Subject; 56 | let decodedMessages: Array<[number, proto.OutboundMessage]>; 57 | 58 | beforeEach(() => { 59 | protobufs$ = new Subject(); 60 | messages = new MessageTransformer(protobufs$, () => {}); 61 | decodedMessages = []; 62 | }); 63 | 64 | it('decodes buffer to OutboundMessage', done => { 65 | messages.outboundMessages$.subscribe({ 66 | next: message => decodedMessages.push(message), 67 | complete: () => { 68 | expect(decodedMessages.length).toBe(1); 69 | const [id, message] = decodedMessages[0]; 70 | expect(id).toBe(1234); 71 | expect(message.message.case).toBe('compileResponse'); 72 | const response = message.message 73 | .value as proto.OutboundMessage_CompileResponse; 74 | expect(response.result.case).toBe('success'); 75 | expect( 76 | ( 77 | response.result 78 | .value as proto.OutboundMessage_CompileResponse_CompileSuccess 79 | ).css, 80 | ).toBe('a {b: c}'); 81 | done(); 82 | }, 83 | }); 84 | 85 | protobufs$.next( 86 | Uint8Array.from([ 87 | ...varint.encode(1234), 88 | ...toBinary( 89 | proto.InboundMessageSchema, 90 | validInboundMessage('a {b: c}'), 91 | ), 92 | ]), 93 | ); 94 | protobufs$.complete(); 95 | }); 96 | 97 | describe('protocol error', () => { 98 | it('fails on invalid buffer', done => { 99 | expectObservableToError( 100 | messages.outboundMessages$, 101 | 'Compiler caused error: Invalid compilation ID varint: RangeError: ' + 102 | 'Could not decode varint.', 103 | done, 104 | ); 105 | 106 | protobufs$.next(Buffer.from([-1])); 107 | }); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /lib/src/message-transformer.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Observable, Subject} from 'rxjs'; 6 | import {map} from 'rxjs/operators'; 7 | import {fromBinary, toBinary} from '@bufbuild/protobuf'; 8 | import * as varint from 'varint'; 9 | 10 | import {compilerError} from './utils'; 11 | import { 12 | InboundMessage, 13 | InboundMessageSchema, 14 | OutboundMessage, 15 | OutboundMessageSchema, 16 | } from './vendor/embedded_sass_pb'; 17 | 18 | /** 19 | * Encodes InboundMessages into protocol buffers and decodes protocol buffers 20 | * into OutboundMessages. 21 | */ 22 | export class MessageTransformer { 23 | // The decoded messages are written to this Subject. It is publicly exposed 24 | // as a readonly Observable. 25 | private readonly outboundMessagesInternal$ = new Subject< 26 | [number, OutboundMessage] 27 | >(); 28 | 29 | /** 30 | * The OutboundMessages, decoded from protocol buffers. If this fails to 31 | * decode a message, it will emit an error. 32 | */ 33 | readonly outboundMessages$ = this.outboundMessagesInternal$.pipe(); 34 | 35 | constructor( 36 | private readonly outboundProtobufs$: Observable, 37 | private readonly writeInboundProtobuf: (buffer: Uint8Array) => void, 38 | ) { 39 | this.outboundProtobufs$ 40 | .pipe(map(decode)) 41 | .subscribe(this.outboundMessagesInternal$); 42 | } 43 | 44 | /** 45 | * Converts the inbound `compilationId` and `message` to a protocol buffer. 46 | */ 47 | writeInboundMessage([compilationId, message]: [ 48 | number, 49 | InboundMessage, 50 | ]): void { 51 | const compilationIdLength = varint.encodingLength(compilationId); 52 | const encodedMessage = toBinary(InboundMessageSchema, message); 53 | const buffer = new Uint8Array(compilationIdLength + encodedMessage.length); 54 | varint.encode(compilationId, buffer); 55 | buffer.set(encodedMessage, compilationIdLength); 56 | 57 | try { 58 | this.writeInboundProtobuf(buffer); 59 | } catch (error) { 60 | this.outboundMessagesInternal$.error(error); 61 | } 62 | } 63 | } 64 | 65 | // Decodes a protobuf `buffer` into a compilation ID and an OutboundMessage, 66 | // ensuring that all mandatory message fields are populated. Throws if `buffer` 67 | // cannot be decoded into a valid message, or if the message itself contains a 68 | // Protocol Error. 69 | function decode(buffer: Uint8Array): [number, OutboundMessage] { 70 | let compilationId: number; 71 | try { 72 | compilationId = varint.decode(buffer); 73 | } catch (error) { 74 | throw compilerError(`Invalid compilation ID varint: ${error}`); 75 | } 76 | 77 | try { 78 | return [ 79 | compilationId, 80 | fromBinary( 81 | OutboundMessageSchema, 82 | new Uint8Array(buffer.buffer, varint.decode.bytes), 83 | ), 84 | ]; 85 | } catch (error) { 86 | throw compilerError(`Invalid protobuf: ${error}`); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/messages.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {InboundMessage, OutboundMessage} from './vendor/embedded_sass_pb'; 6 | 7 | // Given a message type `M` (either `InboundMessage` or `OutboundMessage`) and a 8 | // union of possible message cases `T`, returns all the child types `M` contains 9 | // whose cases match `T`. 10 | type MessagesOfType = (M & { 11 | message: {case: T; value: unknown}; 12 | })['message']['value']; 13 | 14 | /** 15 | * The names of inbound messages that are requests from the host to the 16 | * compiler. 17 | */ 18 | export type InboundRequestType = 'compileRequest'; 19 | 20 | /** Inbound messages that are requests from the host to the compiler. */ 21 | export type InboundRequest = MessagesOfType; 22 | 23 | /** 24 | * The names of inbound of messages that are responses to `OutboundRequest`s. 25 | */ 26 | export type InboundResponseType = 27 | | 'importResponse' 28 | | 'fileImportResponse' 29 | | 'canonicalizeResponse' 30 | | 'functionCallResponse'; 31 | 32 | /** Inbound messages that are responses to `OutboundRequest`s. */ 33 | export type InboundResponse = MessagesOfType< 34 | InboundMessage, 35 | InboundResponseType 36 | >; 37 | 38 | /** 39 | * The names of outbound messages that are requests from the host to the 40 | * compiler. 41 | */ 42 | export type OutboundRequestType = 43 | | 'importRequest' 44 | | 'fileImportRequest' 45 | | 'canonicalizeRequest' 46 | | 'functionCallRequest'; 47 | 48 | /** Outbound messages that are requests from the host to the compiler. */ 49 | export type OutboundRequest = MessagesOfType< 50 | OutboundMessage, 51 | OutboundRequestType 52 | >; 53 | 54 | /** The names of inbound messages that are responses to `InboundRequest`s. */ 55 | export type OutboundResponseType = 'compileResponse'; 56 | 57 | /** Inbound messages that are responses to `InboundRequest`s. */ 58 | export type OutboundResponse = MessagesOfType< 59 | OutboundMessage, 60 | OutboundResponseType 61 | >; 62 | 63 | /** The names of outbound messages that don't require responses. */ 64 | export type OutboundEventType = 'logEvent'; 65 | 66 | /** Outbound messages that don't require responses. */ 67 | export type OutboundEvent = MessagesOfType; 68 | -------------------------------------------------------------------------------- /lib/src/packet-transformer.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Observable, Subject} from 'rxjs'; 6 | 7 | import {PacketTransformer} from './packet-transformer'; 8 | 9 | describe('packet transformer', () => { 10 | let packets: PacketTransformer; 11 | 12 | describe('encode', () => { 13 | let encodedBuffers: Buffer[]; 14 | 15 | beforeEach(() => { 16 | encodedBuffers = []; 17 | packets = new PacketTransformer(new Observable(), buffer => 18 | encodedBuffers.push(buffer), 19 | ); 20 | }); 21 | 22 | it('encodes an empty message', () => { 23 | packets.writeInboundProtobuf(Buffer.from([])); 24 | 25 | expect(encodedBuffers).toEqual([Buffer.from([0])]); 26 | }); 27 | 28 | it('encodes a message of length 1', () => { 29 | packets.writeInboundProtobuf(Buffer.from([123])); 30 | 31 | expect(encodedBuffers).toEqual([Buffer.from([1, 123])]); 32 | }); 33 | 34 | it('encodes a message of length greater than 256', () => { 35 | packets.writeInboundProtobuf(Buffer.alloc(300, 1)); 36 | 37 | expect(encodedBuffers).toEqual([ 38 | Buffer.from([172, 2, ...new Array(300).fill(1)]), 39 | ]); 40 | }); 41 | 42 | it('encodes multiple messages', () => { 43 | packets.writeInboundProtobuf(Buffer.from([10])); 44 | packets.writeInboundProtobuf(Buffer.from([20, 30])); 45 | packets.writeInboundProtobuf(Buffer.from([40, 50, 60])); 46 | 47 | expect(encodedBuffers).toEqual([ 48 | Buffer.from([1, 10]), 49 | Buffer.from([2, 20, 30]), 50 | Buffer.from([3, 40, 50, 60]), 51 | ]); 52 | }); 53 | }); 54 | 55 | describe('decode', () => { 56 | let rawBuffers$: Subject; 57 | 58 | function expectDecoding(expected: Buffer[], done: () => void): void { 59 | const actual: Buffer[] = []; 60 | packets.outboundProtobufs$.subscribe({ 61 | next: protobuf => actual.push(protobuf), 62 | error: () => fail('expected correct decoding'), 63 | complete: () => { 64 | expect(actual).toEqual(expected); 65 | done(); 66 | }, 67 | }); 68 | } 69 | 70 | beforeEach(() => { 71 | rawBuffers$ = new Subject(); 72 | packets = new PacketTransformer(rawBuffers$, () => {}); 73 | }); 74 | 75 | describe('empty message', () => { 76 | it('decodes a single chunk', done => { 77 | expectDecoding([Buffer.from([])], done); 78 | 79 | rawBuffers$.next(Buffer.from([0])); 80 | rawBuffers$.complete(); 81 | }); 82 | 83 | it('decodes a chunk that contains more data', done => { 84 | expectDecoding([Buffer.from([]), Buffer.from([100])], done); 85 | 86 | rawBuffers$.next(Buffer.from([0, 1, 100])); 87 | rawBuffers$.complete(); 88 | }); 89 | }); 90 | 91 | describe('longer message', () => { 92 | it('decodes a single chunk', done => { 93 | expectDecoding([Buffer.from(Buffer.from([1, 2, 3, 4]))], done); 94 | 95 | rawBuffers$.next(Buffer.from([4, 1, 2, 3, 4])); 96 | rawBuffers$.complete(); 97 | }); 98 | 99 | it('decodes multiple chunks', done => { 100 | expectDecoding([Buffer.alloc(300, 1)], done); 101 | 102 | rawBuffers$.next(Buffer.from([172])); 103 | rawBuffers$.next(Buffer.from([2, 1])); 104 | rawBuffers$.next(Buffer.from(Buffer.alloc(299, 1))); 105 | rawBuffers$.complete(); 106 | }); 107 | 108 | it('decodes one chunk per byte', done => { 109 | expectDecoding([Buffer.alloc(300, 1)], done); 110 | 111 | for (const byte of [172, 2, ...Buffer.alloc(300, 1)]) { 112 | rawBuffers$.next(Buffer.from([byte])); 113 | } 114 | rawBuffers$.complete(); 115 | }); 116 | 117 | it('decodes a chunk that contains more data', done => { 118 | expectDecoding([Buffer.from([1, 2, 3, 4]), Buffer.from([0])], done); 119 | 120 | rawBuffers$.next(Buffer.from([4, 1, 2, 3, 4, 1, 0])); 121 | rawBuffers$.complete(); 122 | }); 123 | 124 | it('decodes a full chunk of length greater than 256', done => { 125 | expectDecoding([Buffer.from(new Array(300).fill(1))], done); 126 | 127 | rawBuffers$.next(Buffer.from([172, 2, ...new Array(300).fill(1)])); 128 | rawBuffers$.complete(); 129 | }); 130 | }); 131 | 132 | describe('multiple messages', () => { 133 | it('decodes a single chunk', done => { 134 | expectDecoding( 135 | [Buffer.from([1, 2, 3, 4]), Buffer.from([101, 102])], 136 | done, 137 | ); 138 | 139 | rawBuffers$.next(Buffer.from([4, 1, 2, 3, 4, 2, 101, 102])); 140 | rawBuffers$.complete(); 141 | }); 142 | 143 | it('decodes multiple chunks', done => { 144 | expectDecoding([Buffer.from([1, 2, 3, 4]), Buffer.alloc(300, 1)], done); 145 | 146 | rawBuffers$.next(Buffer.from([4])); 147 | rawBuffers$.next(Buffer.from([1, 2, 3, 4, 172])); 148 | rawBuffers$.next(Buffer.from([2, ...Buffer.alloc(300, 1)])); 149 | rawBuffers$.complete(); 150 | }); 151 | 152 | it('decodes one chunk per byte', done => { 153 | expectDecoding([Buffer.from([1, 2, 3, 4]), Buffer.alloc(300, 1)], done); 154 | 155 | for (const byte of [4, 1, 2, 3, 4, 172, 2, ...Buffer.alloc(300, 1)]) { 156 | rawBuffers$.next(Buffer.from([byte])); 157 | } 158 | rawBuffers$.complete(); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /lib/src/packet-transformer.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Observable, Subject} from 'rxjs'; 6 | import {mergeMap} from 'rxjs/operators'; 7 | import BufferBuilder = require('buffer-builder'); 8 | 9 | /** 10 | * Decodes arbitrarily-chunked buffers, for example 11 | * [ 0 1 2 3 4 5 6 7 ... ], 12 | * into packets of set length in the form 13 | * +---------+------------- ... 14 | * | 0 1 2 3 | 4 5 6 7 ... 15 | * +---------+------------- ... 16 | * | HEADER | PAYLOAD (PROTOBUF) 17 | * +---------+------------- ... 18 | * and emits the payload of each packet. 19 | * 20 | * Encodes packets by attaching a header to a protobuf that describes the 21 | * protobuf's length. 22 | */ 23 | export class PacketTransformer { 24 | // The packet that is actively being decoded as buffers come in. 25 | private packet = new Packet(); 26 | 27 | // The decoded protobufs are written to this Subject. It is publicly exposed 28 | // as a readonly Observable. 29 | private readonly outboundProtobufsInternal$ = new Subject(); 30 | 31 | /** 32 | * The fully-decoded, outbound protobufs. If any errors are encountered 33 | * during encoding/decoding, this Observable will error out. 34 | */ 35 | readonly outboundProtobufs$ = this.outboundProtobufsInternal$.pipe(); 36 | 37 | constructor( 38 | private readonly outboundBuffers$: Observable, 39 | private readonly writeInboundBuffer: (buffer: Buffer) => void, 40 | ) { 41 | this.outboundBuffers$ 42 | .pipe(mergeMap(buffer => this.decode(buffer))) 43 | .subscribe(this.outboundProtobufsInternal$); 44 | } 45 | 46 | /** 47 | * Encodes a packet by pre-fixing `protobuf` with a header that describes its 48 | * length. 49 | */ 50 | writeInboundProtobuf(protobuf: Uint8Array): void { 51 | try { 52 | let length = protobuf.length; 53 | if (length === 0) { 54 | this.writeInboundBuffer(Buffer.alloc(1)); 55 | return; 56 | } 57 | 58 | // Write the length in varint format, 7 bits at a time from least to most 59 | // significant. 60 | const header = new BufferBuilder(8); 61 | while (length > 0) { 62 | // The highest-order bit indicates whether more bytes are necessary to 63 | // fully express the number. The lower 7 bits indicate the number's 64 | // value. 65 | header.appendUInt8((length > 0x7f ? 0x80 : 0) | (length & 0x7f)); 66 | length >>= 7; 67 | } 68 | 69 | const packet = Buffer.alloc(header.length + protobuf.length); 70 | header.copy(packet); 71 | packet.set(protobuf, header.length); 72 | this.writeInboundBuffer(packet); 73 | } catch (error) { 74 | this.outboundProtobufsInternal$.error(error); 75 | } 76 | } 77 | 78 | // Decodes a buffer, filling up the packet that is actively being decoded. 79 | // Returns a list of decoded payloads. 80 | private decode(buffer: Uint8Array): Buffer[] { 81 | const payloads: Buffer[] = []; 82 | let decodedBytes = 0; 83 | while (decodedBytes < buffer.length) { 84 | decodedBytes += this.packet.write(buffer.slice(decodedBytes)); 85 | if (this.packet.isComplete && this.packet.payload) { 86 | payloads.push(this.packet.payload); 87 | this.packet = new Packet(); 88 | } 89 | } 90 | return payloads; 91 | } 92 | } 93 | 94 | /** A length-delimited packet comprised of a header and payload. */ 95 | class Packet { 96 | // The number of bits we've consumed so far to fill out `payloadLength`. 97 | private payloadLengthBits = 0; 98 | 99 | // The length of the next message, in bytes. 100 | // 101 | // This is built up from a [varint]. Once it's fully consumed, `payload` is 102 | // initialized and this is reset to 0. 103 | // 104 | // [varint]: https://developers.google.com/protocol-buffers/docs/encoding#varints 105 | private payloadLength = 0; 106 | 107 | /** 108 | * The packet's payload. Constructed by calls to write(). 109 | * @see write 110 | */ 111 | payload?: Buffer; 112 | 113 | // The offset in [payload] that should be written to next time data arrives. 114 | private payloadOffset = 0; 115 | 116 | /** Whether the packet construction is complete. */ 117 | get isComplete(): boolean { 118 | return !!(this.payload && this.payloadOffset >= this.payloadLength); 119 | } 120 | 121 | /** 122 | * Takes arbitrary binary input and slots it into the header and payload 123 | * appropriately. Returns the number of bytes that were written into the 124 | * packet. This method can be called repeatedly, incrementally building 125 | * up the packet until it is complete. 126 | */ 127 | write(source: Uint8Array): number { 128 | if (this.isComplete) { 129 | throw Error('Cannot write to a completed Packet.'); 130 | } 131 | 132 | // The index of the next byte to read from [source]. We have to track this 133 | // because the source may contain the length *and* the message. 134 | let i = 0; 135 | 136 | // We can be in one of two states here: 137 | // 138 | // * [payload] is `null`, in which case we're adding data to [payloadLength] 139 | // until we reach a byte with its most significant bit set to 0. 140 | // 141 | // * [payload] is not `null`, in which case we're waiting for 142 | // [payloadOffset] to reach [payloadLength] bytes in it so this packet is 143 | // complete. 144 | if (!this.payload) { 145 | for (;;) { 146 | const byte = source[i]; 147 | 148 | // Varints encode data in the 7 lower bits of each byte, which we access 149 | // by masking with 0x7f = 0b01111111. 150 | this.payloadLength += (byte & 0x7f) << this.payloadLengthBits; 151 | this.payloadLengthBits += 7; 152 | i++; 153 | 154 | if (byte <= 0x7f) { 155 | // If the byte is lower than 0x7f = 0b01111111, that means its high 156 | // bit is unset which and we now know the full message length and can 157 | // initialize [this.payload]. 158 | this.payload = Buffer.alloc(this.payloadLength); 159 | break; 160 | } else if (i === source.length) { 161 | // If we've hit the end of the source chunk, we need to wait for the 162 | // next chunk to arrive. Just return the number of bytes we've 163 | // consumed so far. 164 | return i; 165 | } else { 166 | // Otherwise, we continue reading bytes from the source data to fill 167 | // in [this.payloadLength]. 168 | } 169 | } 170 | } 171 | 172 | // Copy as many bytes as we can from [source] to [payload], making sure not 173 | // to try to copy more than the payload can hold (if the source has another 174 | // message after the current one) or more than the source has available (if 175 | // the current message is split across multiple chunks). 176 | const bytesToWrite = Math.min( 177 | this.payload.length - this.payloadOffset, 178 | source.length - i, 179 | ); 180 | this.payload.set(source.subarray(i, i + bytesToWrite), this.payloadOffset); 181 | this.payloadOffset += bytesToWrite; 182 | 183 | return i + bytesToWrite; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/src/request-tracker.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {RequestTracker} from './request-tracker'; 6 | 7 | describe('request tracker', () => { 8 | let tracker: RequestTracker; 9 | 10 | beforeEach(() => { 11 | tracker = new RequestTracker(); 12 | }); 13 | 14 | it('returns the next ID when empty', () => { 15 | expect(tracker.nextId).toBe(0); 16 | }); 17 | 18 | describe('tracking requests', () => { 19 | it('tracks when empty', () => { 20 | tracker.add(0, 'compileResponse'); 21 | expect(tracker.nextId).toBe(1); 22 | }); 23 | 24 | it('tracks multiple requests', () => { 25 | tracker.add(0, 'compileResponse'); 26 | tracker.add(1, 'compileResponse'); 27 | tracker.add(2, 'compileResponse'); 28 | expect(tracker.nextId).toBe(3); 29 | }); 30 | 31 | it('tracks starting from a non-zero ID', () => { 32 | tracker.add(1, 'compileResponse'); 33 | expect(tracker.nextId).toBe(0); 34 | tracker.add(0, 'compileResponse'); 35 | expect(tracker.nextId).toBe(2); 36 | }); 37 | 38 | it('errors if the request ID is invalid', () => { 39 | expect(() => tracker.add(-1, 'compileResponse')).toThrowError( 40 | 'Invalid request ID -1.', 41 | ); 42 | }); 43 | 44 | it('errors if the request ID overlaps that of an existing in-flight request', () => { 45 | tracker.add(0, 'compileResponse'); 46 | expect(() => tracker.add(0, 'compileResponse')).toThrowError( 47 | 'Request ID 0 is already in use by an in-flight request.', 48 | ); 49 | }); 50 | }); 51 | 52 | describe('resolving requests', () => { 53 | it('resolves a single request', () => { 54 | tracker.add(0, 'compileResponse'); 55 | tracker.resolve(0, 'compileResponse'); 56 | expect(tracker.nextId).toBe(0); 57 | }); 58 | 59 | it('resolves multiple requests', () => { 60 | tracker.add(0, 'compileResponse'); 61 | tracker.add(1, 'compileResponse'); 62 | tracker.add(2, 'compileResponse'); 63 | tracker.resolve(1, 'compileResponse'); 64 | tracker.resolve(2, 'compileResponse'); 65 | tracker.resolve(0, 'compileResponse'); 66 | expect(tracker.nextId).toBe(0); 67 | }); 68 | 69 | it('reuses the ID of a resolved request', () => { 70 | tracker.add(0, 'compileResponse'); 71 | tracker.add(1, 'compileResponse'); 72 | tracker.resolve(0, 'compileResponse'); 73 | expect(tracker.nextId).toBe(0); 74 | }); 75 | 76 | it('errors if the response ID does not match any existing request IDs', () => { 77 | expect(() => tracker.resolve(0, 'compileResponse')).toThrowError( 78 | 'Response ID 0 does not match any pending requests.', 79 | ); 80 | }); 81 | 82 | it('errors if the response type does not match what the request is expecting', () => { 83 | tracker.add(0, 'importResponse'); 84 | expect(() => tracker.resolve(0, 'fileImportResponse')).toThrowError( 85 | "Response with ID 0 does not match pending request's type. Expected " + 86 | 'importResponse but received fileImportResponse.', 87 | ); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /lib/src/request-tracker.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {InboundResponseType, OutboundResponseType} from './messages'; 6 | 7 | /** 8 | * Manages pending inbound and outbound requests. Ensures that requests and 9 | * responses interact correctly and obey the Embedded Protocol. 10 | */ 11 | export class RequestTracker { 12 | // The indices of this array correspond to each pending request's ID. Stores 13 | // the response type expected by each request. 14 | private readonly requests: Array< 15 | InboundResponseType | OutboundResponseType | null 16 | > = []; 17 | 18 | /** The next available request ID. */ 19 | get nextId(): number { 20 | for (let i = 0; i < this.requests.length; i++) { 21 | if (this.requests[i] === undefined || this.requests[i] === null) { 22 | return i; 23 | } 24 | } 25 | return this.requests.length; 26 | } 27 | 28 | /** 29 | * Adds an entry for a pending request with ID `id`. The entry stores the 30 | * expected response type. Throws an error if the Protocol Error is violated. 31 | */ 32 | add( 33 | id: number, 34 | expectedResponseType: InboundResponseType | OutboundResponseType, 35 | ): void { 36 | if (id < 0) { 37 | throw Error(`Invalid request ID ${id}.`); 38 | } else if (this.requests[id]) { 39 | throw Error( 40 | `Request ID ${id} is already in use by an in-flight request.`, 41 | ); 42 | } 43 | this.requests[id] = expectedResponseType; 44 | } 45 | 46 | /** 47 | * Resolves a pending request with matching ID `id` and expected response type 48 | * `type`. Throws an error if the Protocol Error is violated. 49 | */ 50 | resolve(id: number, type: InboundResponseType | OutboundResponseType): void { 51 | if (this.requests[id] === undefined || this.requests[id] === null) { 52 | throw Error(`Response ID ${id} does not match any pending requests.`); 53 | } else if (this.requests[id] !== type) { 54 | throw Error( 55 | `Response with ID ${id} does not match pending request's type. Expected ${this.requests[id]} but received ${type}.`, 56 | ); 57 | } 58 | this.requests[id] = null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import {pathToFileURL} from 'url'; 2 | import {pathToUrlString} from './utils'; 3 | 4 | describe('utils', () => { 5 | describe('pathToUrlString', () => { 6 | it('encode relative path like `pathToFileURL`', () => { 7 | const baseURL = pathToFileURL('').toString(); 8 | // Skip charcodes 0-32 to work around Node trailing whitespace regression: 9 | // https://github.com/nodejs/node/issues/51167 10 | for (let i = 33; i < 128; i++) { 11 | const char = String.fromCharCode(i); 12 | const filename = `${i}-${char}`; 13 | expect(pathToUrlString(filename)).toEqual( 14 | pathToFileURL(filename) 15 | .toString() 16 | .slice(baseURL.length + 1), 17 | ); 18 | } 19 | }); 20 | 21 | it('encode percent encoded string like `pathToFileURL`', () => { 22 | const baseURL = pathToFileURL('').toString(); 23 | for (let i = 0; i < 128; i++) { 24 | const lowercase = `%${i < 10 ? '0' : ''}${i.toString(16)}`; 25 | expect(pathToUrlString(lowercase)).toEqual( 26 | pathToFileURL(lowercase) 27 | .toString() 28 | .slice(baseURL.length + 1), 29 | ); 30 | const uppercase = lowercase.toUpperCase(); 31 | expect(pathToUrlString(uppercase)).toEqual( 32 | pathToFileURL(uppercase) 33 | .toString() 34 | .slice(baseURL.length + 1), 35 | ); 36 | } 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /lib/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {List} from 'immutable'; 6 | import * as p from 'path'; 7 | import * as url from 'url'; 8 | 9 | import * as proto from './vendor/embedded_sass_pb'; 10 | import {Syntax} from './vendor/sass'; 11 | 12 | export type PromiseOr< 13 | T, 14 | sync extends 'sync' | 'async' = 'async', 15 | > = sync extends 'async' ? T | Promise : T; 16 | 17 | // A boolean type that's `true` if `sync` requires synchronous APIs only and 18 | // `false` if it allows asynchronous APIs. 19 | export type SyncBoolean = sync extends 'async' 20 | ? false 21 | : true; 22 | 23 | /** 24 | * The equivalent of `Promise.then()`, except that if the first argument is a 25 | * plain value it synchronously invokes `callback()` and returns its result. 26 | */ 27 | export function thenOr( 28 | promiseOrValue: PromiseOr, 29 | callback: (value: T) => PromiseOr, 30 | ): PromiseOr { 31 | return promiseOrValue instanceof Promise 32 | ? (promiseOrValue.then(callback) as PromiseOr) 33 | : callback(promiseOrValue as T); 34 | } 35 | 36 | /** 37 | * The equivalent of `Promise.catch()`, except that if the first argument throws 38 | * synchronously it synchronously invokes `callback()` and returns its result. 39 | */ 40 | export function catchOr( 41 | promiseOrValueCallback: () => PromiseOr, 42 | callback: (error: unknown) => PromiseOr, 43 | ): PromiseOr { 44 | try { 45 | const result = promiseOrValueCallback(); 46 | return result instanceof Promise 47 | ? (result.catch(callback) as PromiseOr) 48 | : result; 49 | } catch (error: unknown) { 50 | return callback(error); 51 | } 52 | } 53 | 54 | /** Checks for null or undefined. */ 55 | export function isNullOrUndefined( 56 | object: T | null | undefined, 57 | ): object is null | undefined { 58 | return object === null || object === undefined; 59 | } 60 | 61 | /** Returns `collection` as an immutable List. */ 62 | export function asImmutableList(collection: T[] | List): List { 63 | return List.isList(collection) ? collection : List(collection); 64 | } 65 | 66 | /** Constructs a compiler-caused Error. */ 67 | export function compilerError(message: string): Error { 68 | return Error(`Compiler caused error: ${message}.`); 69 | } 70 | 71 | /** 72 | * Returns a `compilerError()` indicating that the given `field` should have 73 | * been included but was not. 74 | */ 75 | export function mandatoryError(field: string): Error { 76 | return compilerError(`Missing mandatory field ${field}`); 77 | } 78 | 79 | /** Constructs a host-caused Error. */ 80 | export function hostError(message: string): Error { 81 | return Error(`Compiler reported error: ${message}.`); 82 | } 83 | 84 | /** Constructs an error caused by an invalid value type. */ 85 | export function valueError(message: string, name?: string): Error { 86 | return Error(name ? `$${name}: ${message}.` : `${message}.`); 87 | } 88 | 89 | // Node changed its implementation of pathToFileURL: 90 | // https://github.com/nodejs/node/pull/54545 91 | const unsafePathToFileURL = url.pathToFileURL('~').pathname.endsWith('~'); 92 | 93 | /** Converts a (possibly relative) path on the local filesystem to a URL. */ 94 | export function pathToUrlString(path: string): string { 95 | if (p.isAbsolute(path)) return url.pathToFileURL(path).toString(); 96 | 97 | // percent encode relative path like `pathToFileURL` 98 | let fileUrl = encodeURI(path).replace(/[#?]/g, encodeURIComponent); 99 | 100 | if (unsafePathToFileURL) { 101 | fileUrl = fileUrl.replace(/%(5B|5D|5E|7C)/g, decodeURIComponent); 102 | } else { 103 | fileUrl = fileUrl.replace(/~/g, '%7E'); 104 | } 105 | 106 | if (process.platform === 'win32') { 107 | fileUrl = fileUrl.replace(/%5C/g, '/'); 108 | } 109 | 110 | return fileUrl; 111 | } 112 | 113 | /** 114 | * Like `url.fileURLToPath`, but returns the same result for Windows-style file 115 | * URLs on all platforms. 116 | */ 117 | export function fileUrlToPathCrossPlatform(fileUrl: url.URL | string): string { 118 | const path = url.fileURLToPath(fileUrl); 119 | 120 | // Windows file: URLs begin with `file:///C:/` (or another drive letter), 121 | // which `fileURLToPath` converts to `"/C:/"` on non-Windows systems. We want 122 | // to ensure the behavior is consistent across OSes, so we normalize this back 123 | // to a Windows-style path. 124 | return /^\/[A-Za-z]:\//.test(path) ? path.substring(1) : path; 125 | } 126 | 127 | /** Returns `path` without an extension, if it had one. */ 128 | export function withoutExtension(path: string): string { 129 | const extension = p.extname(path); 130 | return path.substring(0, path.length - extension.length); 131 | } 132 | 133 | /** Converts a JS syntax string into a protobuf syntax enum. */ 134 | export function protofySyntax(syntax: Syntax): proto.Syntax { 135 | switch (syntax) { 136 | case 'scss': 137 | return proto.Syntax.SCSS; 138 | 139 | case 'indented': 140 | return proto.Syntax.INDENTED; 141 | 142 | case 'css': 143 | return proto.Syntax.CSS; 144 | 145 | default: 146 | throw new Error(`Unknown syntax: "${syntax}"`); 147 | } 148 | } 149 | 150 | /** Returns whether `error` is a NodeJS-style exception with an error code. */ 151 | export function isErrnoException( 152 | error: unknown, 153 | ): error is NodeJS.ErrnoException { 154 | return error instanceof Error && ('errno' in error || 'code' in error); 155 | } 156 | 157 | /** 158 | * Dart-style utility. See 159 | * http://go/dart-api/stable/2.8.4/dart-core/Map/putIfAbsent.html. 160 | */ 161 | export function putIfAbsent( 162 | map: Map, 163 | key: K, 164 | provider: () => V, 165 | ): V { 166 | const val = map.get(key); 167 | if (val !== undefined) { 168 | return val; 169 | } else { 170 | const newVal = provider(); 171 | map.set(key, newVal); 172 | return newVal; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/src/value/argument-list.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {List, OrderedMap, isOrderedMap} from 'immutable'; 6 | 7 | import {ListSeparator, SassList} from './list'; 8 | import {Value} from './index'; 9 | 10 | export class SassArgumentList extends SassList { 11 | /** 12 | * The `FunctionCallRequest`-scoped ID of this argument list, used to tell the 13 | * compiler which argument lists have had their keywords accessed during a 14 | * function call. 15 | * 16 | * The special undefined indicates an argument list constructed in the host. 17 | * 18 | * This is marked as public so that the protofier can access it, but it's not 19 | * part of the package's public API and should not be accessed by user code. 20 | * It may be renamed or removed without warning in the future. 21 | */ 22 | readonly id: number | undefined; 23 | 24 | /** 25 | * If this argument list is constructed in the compiler, this is the unique 26 | * context that the host uses to determine which compilation this argument 27 | * list belongs to. 28 | * 29 | * This is marked as public so that the protofier can access it, but it's not 30 | * part of the package's public API and should not be accessed by user code. 31 | * It may be renamed or removed without warning in the future. 32 | */ 33 | readonly compileContext: symbol | undefined; 34 | 35 | /** 36 | * The argument list's keywords. This isn't exposed directly so that we can 37 | * set `keywordsAccessed` when the user reads it. 38 | * 39 | * This is marked as public so that the protofier can access it, but it's not 40 | * part of the package's public API and should not be accessed by user code. 41 | * It may be renamed or removed without warning in the future. 42 | */ 43 | readonly keywordsInternal: OrderedMap; 44 | 45 | private _keywordsAccessed = false; 46 | 47 | /** 48 | * Whether the `keywords` getter has been accessed. 49 | * 50 | * This is marked as public so that the protofier can access it, but it's not 51 | * part of the package's public API and should not be accessed by user code. 52 | * It may be renamed or removed without warning in the future. 53 | */ 54 | get keywordsAccessed(): boolean { 55 | return this._keywordsAccessed; 56 | } 57 | 58 | get keywords(): OrderedMap { 59 | this._keywordsAccessed = true; 60 | return this.keywordsInternal; 61 | } 62 | 63 | constructor( 64 | contents: Value[] | List, 65 | keywords: Record | OrderedMap, 66 | separator?: ListSeparator, 67 | id?: number, 68 | compileContext?: symbol, 69 | ) { 70 | super(contents, {separator}); 71 | this.keywordsInternal = isOrderedMap(keywords) 72 | ? keywords 73 | : OrderedMap(keywords); 74 | this.id = id; 75 | this.compileContext = compileContext; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/value/boolean.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {hash} from 'immutable'; 6 | 7 | import {Value} from './index'; 8 | 9 | /** 10 | * Sass boolean. 11 | * 12 | * This is an abstract class that cannot be directly instantiated. Instead, 13 | * use the provided {@link sassTrue} and {@link sassFalse} singleton instances. 14 | */ 15 | export abstract class SassBoolean extends Value { 16 | abstract readonly value: boolean; 17 | } 18 | 19 | const trueHash = hash(true); 20 | const falseHash = hash(false); 21 | 22 | export class SassBooleanInternal extends SassBoolean { 23 | // Whether callers are allowed to construct this class. This is set to 24 | // `false` once the two constants are constructed so that the constructor 25 | // throws an error for future calls, in accordance with the legacy API. 26 | static constructionAllowed = true; 27 | 28 | constructor(private readonly valueInternal: boolean) { 29 | super(); 30 | 31 | if (!SassBooleanInternal.constructionAllowed) { 32 | throw ( 33 | "new sass.types.Boolean() isn't allowed.\n" + 34 | 'Use sass.types.Boolean.TRUE or sass.types.Boolean.FALSE instead.' 35 | ); 36 | } 37 | 38 | Object.freeze(this); 39 | } 40 | 41 | get value(): boolean { 42 | return this.valueInternal; 43 | } 44 | 45 | get isTruthy(): boolean { 46 | return this.value; 47 | } 48 | 49 | assertBoolean(): SassBoolean { 50 | return this; 51 | } 52 | 53 | equals(other: Value): boolean { 54 | return this === other; 55 | } 56 | 57 | hashCode(): number { 58 | return this.value ? trueHash : falseHash; 59 | } 60 | 61 | toString(): string { 62 | return this.value ? 'sassTrue' : 'sassFalse'; 63 | } 64 | 65 | // Legacy API support 66 | 67 | static TRUE: SassBooleanInternal; 68 | static FALSE: SassBooleanInternal; 69 | 70 | getValue(): boolean { 71 | return this.value; 72 | } 73 | } 74 | 75 | /** The singleton instance of SassScript true. */ 76 | export const sassTrue = new SassBooleanInternal(true); 77 | 78 | /** The singleton instance of SassScript false. */ 79 | export const sassFalse = new SassBooleanInternal(false); 80 | 81 | // Legacy API support 82 | SassBooleanInternal.constructionAllowed = false; 83 | 84 | SassBooleanInternal.TRUE = sassTrue; 85 | SassBooleanInternal.FALSE = sassFalse; 86 | -------------------------------------------------------------------------------- /lib/src/value/calculations.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {List, ValueObject, hash} from 'immutable'; 6 | 7 | import {Value} from './index'; 8 | import {SassNumber} from './number'; 9 | import {SassString} from './string'; 10 | 11 | export type CalculationValue = 12 | | SassNumber 13 | | SassCalculation 14 | | SassString 15 | | CalculationOperation 16 | | CalculationInterpolation; 17 | 18 | type CalculationValueIterable = CalculationValue[] | List; 19 | 20 | function assertCalculationValue(value: CalculationValue): void { 21 | if (value instanceof SassString && value.hasQuotes) { 22 | throw new Error(`Expected ${value} to be an unquoted string.`); 23 | } 24 | } 25 | 26 | function isValidClampArg(value: CalculationValue): boolean { 27 | return ( 28 | value instanceof CalculationInterpolation || 29 | (value instanceof SassString && !value.hasQuotes) 30 | ); 31 | } 32 | 33 | /* A SassScript calculation */ 34 | export class SassCalculation extends Value { 35 | readonly arguments: List; 36 | 37 | private constructor( 38 | readonly name: string, 39 | args: CalculationValueIterable, 40 | ) { 41 | super(); 42 | this.arguments = List(args); 43 | } 44 | 45 | static calc(argument: CalculationValue): SassCalculation { 46 | assertCalculationValue(argument); 47 | return new SassCalculation('calc', [argument]); 48 | } 49 | 50 | static min(args: CalculationValueIterable): SassCalculation { 51 | args.forEach(assertCalculationValue); 52 | return new SassCalculation('min', args); 53 | } 54 | 55 | static max(args: CalculationValueIterable): SassCalculation { 56 | args.forEach(assertCalculationValue); 57 | return new SassCalculation('max', args); 58 | } 59 | 60 | static clamp( 61 | min: CalculationValue, 62 | value?: CalculationValue, 63 | max?: CalculationValue, 64 | ): SassCalculation { 65 | if ( 66 | (value === undefined && !isValidClampArg(min)) || 67 | (max === undefined && ![min, value].some(x => x && isValidClampArg(x))) 68 | ) { 69 | throw new Error( 70 | 'Argument must be an unquoted SassString or CalculationInterpolation.', 71 | ); 72 | } 73 | const args = [min]; 74 | if (value !== undefined) args.push(value); 75 | if (max !== undefined) args.push(max); 76 | args.forEach(assertCalculationValue); 77 | return new SassCalculation('clamp', args); 78 | } 79 | 80 | assertCalculation(): SassCalculation { 81 | return this; 82 | } 83 | 84 | equals(other: unknown): boolean { 85 | return ( 86 | other instanceof SassCalculation && 87 | this.name === other.name && 88 | this.arguments.equals(other.arguments) 89 | ); 90 | } 91 | 92 | hashCode(): number { 93 | return hash(this.name) ^ this.arguments.hashCode(); 94 | } 95 | 96 | toString(): string { 97 | return `${this.name}(${this.arguments.join(', ')})`; 98 | } 99 | } 100 | 101 | const operators = ['+', '-', '*', '/'] as const; 102 | export type CalculationOperator = (typeof operators)[number]; 103 | 104 | export class CalculationOperation implements ValueObject { 105 | constructor( 106 | readonly operator: CalculationOperator, 107 | readonly left: CalculationValue, 108 | readonly right: CalculationValue, 109 | ) { 110 | if (!operators.includes(operator)) { 111 | throw new Error(`Invalid operator: ${operator}`); 112 | } 113 | assertCalculationValue(left); 114 | assertCalculationValue(right); 115 | } 116 | 117 | equals(other: unknown): boolean { 118 | return ( 119 | other instanceof CalculationOperation && 120 | this.operator === other.operator && 121 | this.left === other.left && 122 | this.right === other.right 123 | ); 124 | } 125 | 126 | hashCode(): number { 127 | return hash(this.operator) ^ hash(this.left) ^ hash(this.right); 128 | } 129 | } 130 | 131 | export class CalculationInterpolation implements ValueObject { 132 | constructor(readonly value: string) {} 133 | 134 | equals(other: unknown): boolean { 135 | return ( 136 | other instanceof CalculationInterpolation && this.value === other.value 137 | ); 138 | } 139 | 140 | hashCode(): number { 141 | return hash(this.value); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/src/value/function.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {hash} from 'immutable'; 6 | 7 | import {CustomFunction} from '../vendor/sass'; 8 | import {Value} from './index'; 9 | 10 | /** A first-class SassScript function. */ 11 | export class SassFunction extends Value { 12 | /** 13 | * If this function is defined in the compiler, this is the unique ID that the 14 | * compiler uses to determine which function it refers to. 15 | * 16 | * This is marked as public so that the protofier can access it, but it's not 17 | * part of the package's public API and should not be accessed by user code. 18 | * It may be renamed or removed without warning in the future. 19 | */ 20 | readonly id: number | undefined; 21 | 22 | /** 23 | * If this function is defined in the compiler, this is the unique context 24 | * that the host uses to determine which compilation this function belongs to. 25 | * 26 | * This is marked as public so that the protofier can access it, but it's not 27 | * part of the package's public API and should not be accessed by user code. 28 | * It may be renamed or removed without warning in the future. 29 | */ 30 | readonly compileContext: symbol | undefined; 31 | 32 | /** 33 | * If this function is defined in the host, this is the signature that 34 | * describes how to pass arguments to it. 35 | * 36 | * This is marked as public so that the protofier can access it, but it's not 37 | * part of the package's public API and should not be accessed by user code. 38 | * It may be renamed or removed without warning in the future. 39 | */ 40 | readonly signature: string | undefined; 41 | 42 | /** 43 | * If this function is defined in the host, this is the callback to run when 44 | * the function is invoked from a stylesheet. 45 | * 46 | * This is marked as public so that the protofier can access it, but it's not 47 | * part of the package's public API and should not be accessed by user code. 48 | * It may be renamed or removed without warning in the future. 49 | */ 50 | readonly callback: CustomFunction<'sync'> | undefined; 51 | 52 | constructor(id: number, compileContext: symbol); 53 | constructor(signature: string, callback: CustomFunction<'sync'>); 54 | constructor( 55 | idOrSignature: number | string, 56 | callbackOrCompileContext: CustomFunction<'sync'> | symbol, 57 | ) { 58 | super(); 59 | 60 | if ( 61 | typeof idOrSignature === 'number' && 62 | typeof callbackOrCompileContext === 'symbol' 63 | ) { 64 | this.id = idOrSignature; 65 | this.compileContext = callbackOrCompileContext; 66 | } else { 67 | this.signature = idOrSignature as string; 68 | this.callback = callbackOrCompileContext as CustomFunction<'sync'>; 69 | } 70 | } 71 | 72 | equals(other: Value): boolean { 73 | return this.id === undefined 74 | ? other === this 75 | : other instanceof SassFunction && 76 | other.compileContext === this.compileContext && 77 | other.id === this.id; 78 | } 79 | 80 | hashCode(): number { 81 | return this.id === undefined ? hash(this.signature) : hash(this.id); 82 | } 83 | 84 | toString(): string { 85 | return this.signature ? this.signature : ``; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/value/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {List, ValueObject} from 'immutable'; 6 | 7 | import {ListSeparator} from './list'; 8 | import {SassBoolean} from './boolean'; 9 | import {SassColor} from './color'; 10 | import {SassMap} from './map'; 11 | import {SassNumber} from './number'; 12 | import {SassString} from './string'; 13 | import {valueError} from '../utils'; 14 | import {SassCalculation} from './calculations'; 15 | import {SassMixin} from './mixin'; 16 | 17 | /** 18 | * A SassScript value. 19 | * 20 | * All SassScript values are immutable. 21 | * 22 | * Concrete values (such as `SassColor`) are implemented as subclasses and get 23 | * instantiated as normal JS classes. 24 | * 25 | * Untyped values can be cast to particular types using `assert*()` functions, 26 | * which throw user-friendly error messages if they fail. 27 | * 28 | * All values, except `false` and `null`, count as `true`. 29 | * 30 | * All values can be used as lists. Maps count as lists of pairs, while all 31 | * other values count as single-value lists. Empty maps are equal to empty 32 | * lists. 33 | */ 34 | export abstract class Value implements ValueObject { 35 | /** Whether `this` counts as `true`. */ 36 | get isTruthy(): boolean { 37 | return true; 38 | } 39 | 40 | /** Returns JS null if `this` is `sassNull`. Otherwise, returns `this`. */ 41 | get realNull(): Value | null { 42 | return this; 43 | } 44 | 45 | /** `this` as a list. */ 46 | get asList(): List { 47 | return List([this]); 48 | } 49 | 50 | /** The separator for `this` as a list. */ 51 | get separator(): ListSeparator { 52 | return null; 53 | } 54 | 55 | /** Whether `this`, as a list, has brackets. */ 56 | get hasBrackets(): boolean { 57 | return false; 58 | } 59 | 60 | // Subclasses can override this to change the behavior of 61 | // `sassIndexToListIndex`. 62 | protected get lengthAsList(): number { 63 | return 1; 64 | } 65 | 66 | /** 67 | * Converts `sassIndex` to a JS index into the array returned by `asList`. 68 | * 69 | * Sass indices start counting at 1, and may be negative in order to index 70 | * from the end of the list. 71 | * 72 | * `sassIndex` must be... 73 | * - a number, and 74 | * - an integer, and 75 | * - a valid index into `asList`. 76 | * 77 | * Otherwise, this throws an error. 78 | * 79 | * If `this` came from a function argument, `name` is the argument name 80 | * (without the `$`) and is used for error reporting. 81 | */ 82 | sassIndexToListIndex(sassIndex: Value, name?: string): number { 83 | const index = sassIndex.assertNumber().assertInt(); 84 | if (index === 0) { 85 | throw Error('List index may not be 0.'); 86 | } 87 | if (Math.abs(index) > this.lengthAsList) { 88 | throw valueError( 89 | `Invalid index ${sassIndex} for a list with ${this.lengthAsList} elements.`, 90 | name, 91 | ); 92 | } 93 | return index < 0 ? this.lengthAsList + index : index - 1; 94 | } 95 | 96 | /** Returns `this.asList.get(index)`. */ 97 | get(index: number): Value | undefined { 98 | return index < 1 && index >= -1 ? this : undefined; 99 | } 100 | 101 | /** 102 | * Casts `this` to `SassBoolean`; throws if `this` isn't a boolean. 103 | * 104 | * If `this` came from a function argument, `name` is the argument name 105 | * (without the `$`) and is used for error reporting. 106 | */ 107 | assertBoolean(name?: string): SassBoolean { 108 | throw valueError(`${this} is not a boolean`, name); 109 | } 110 | 111 | /** 112 | * Casts `this` to `SassCalculation`; throws if `this` isn't a calculation. 113 | * 114 | * If `this` came from a function argument, `name` is the argument name 115 | * (without the `$`) and is used for error reporting. 116 | */ 117 | assertCalculation(name?: string): SassCalculation { 118 | throw valueError(`${this} is not a calculation`, name); 119 | } 120 | 121 | /** 122 | * Casts `this` to `SassColor`; throws if `this` isn't a color. 123 | * 124 | * If `this` came from a function argument, `name` is the argument name 125 | * (without the `$`) and is used for error reporting. 126 | */ 127 | assertColor(name?: string): SassColor { 128 | throw valueError(`${this} is not a color`, name); 129 | } 130 | 131 | /** 132 | * Casts `this` to `SassFunction`; throws if `this` isn't a function 133 | * reference. 134 | * 135 | * If `this` came from a function argument, `name` is the argument name 136 | * (without the `$`) and is used for error reporting. 137 | */ 138 | assertFunction(name?: string): Value { 139 | throw valueError(`${this} is not a function reference`, name); 140 | // TODO(awjin): Narrow the return type to SassFunction. 141 | } 142 | 143 | /** 144 | * Casts `this` to `SassMixin`; throws if `this` isn't a mixin 145 | * reference. 146 | * 147 | * If `this` came from a function argument, `name` is the argument name 148 | * (without the `$`) and is used for error reporting. 149 | */ 150 | assertMixin(name?: string): SassMixin { 151 | throw valueError(`${this} is not a mixin reference`, name); 152 | } 153 | 154 | /** 155 | * Casts `this` to `SassMap`; throws if `this` isn't a map. 156 | * 157 | * If `this` came from a function argument, `name` is the argument name 158 | * (without the `$`) and is used for error reporting. 159 | */ 160 | assertMap(name?: string): SassMap { 161 | throw valueError(`${this} is not a map`, name); 162 | } 163 | 164 | /** 165 | * Returns `this` as a `SassMap` if it counts as one (including empty lists), 166 | * or `null` if it does not. 167 | */ 168 | tryMap(): SassMap | null { 169 | return null; 170 | } 171 | 172 | /** 173 | * Casts `this` to `SassString`; throws if `this` isn't a string. 174 | * 175 | * If `this` came from a function argument, `name` is the argument name 176 | * (without the `$`) and is used for error reporting. 177 | */ 178 | assertNumber(name?: string): SassNumber { 179 | throw valueError(`${this} is not a number`, name); 180 | } 181 | 182 | /** 183 | * Casts `this` to `SassString`; throws if `this` isn't a string. 184 | * 185 | * If `this` came from a function argument, `name` is the argument name 186 | * (without the `$`) and is used for error reporting. 187 | */ 188 | assertString(name?: string): SassString { 189 | throw valueError(`${this} is not a string`, name); 190 | } 191 | 192 | /** Whether `this == other` in SassScript. */ 193 | abstract equals(other: Value): boolean; 194 | 195 | /** This is the same for values that are `==` in SassScript. */ 196 | abstract hashCode(): number; 197 | 198 | /** A meaningful descriptor for this value. */ 199 | abstract toString(): string; 200 | } 201 | -------------------------------------------------------------------------------- /lib/src/value/list.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {List, hash, isList} from 'immutable'; 6 | 7 | import {Value} from './index'; 8 | import {SassMap} from './map'; 9 | import {asImmutableList, valueError} from '../utils'; 10 | 11 | /** The types of separator that a SassList can have. */ 12 | export type ListSeparator = ',' | '/' | ' ' | null; 13 | 14 | // All empty SassList and SassMaps should have the same hashcode, so this caches 15 | // the value. 16 | const emptyListHashCode = hash([]); 17 | 18 | /** The options that are passed to the constructor. */ 19 | interface ConstructorOptions { 20 | separator?: ListSeparator; 21 | brackets?: boolean; 22 | } 23 | 24 | /** A SassScript list. */ 25 | export class SassList extends Value { 26 | private readonly contentsInternal: List; 27 | private readonly separatorInternal: ListSeparator; 28 | private readonly hasBracketsInternal: boolean; 29 | 30 | /** 31 | * Returns a list that contains `contents`, with the given `separator` and 32 | * `brackets`. 33 | */ 34 | constructor( 35 | contents: Value[] | List, 36 | options?: { 37 | separator?: ListSeparator; 38 | brackets?: boolean; 39 | }, 40 | ); 41 | constructor(options?: ConstructorOptions); 42 | constructor( 43 | contentsOrOptions?: Value[] | List | ConstructorOptions, 44 | options?: ConstructorOptions, 45 | ) { 46 | super(); 47 | 48 | if (isList(contentsOrOptions) || Array.isArray(contentsOrOptions)) { 49 | this.contentsInternal = asImmutableList(contentsOrOptions); 50 | } else { 51 | this.contentsInternal = List(); 52 | options = contentsOrOptions; 53 | } 54 | 55 | if (this.contentsInternal.size > 1 && options?.separator === null) { 56 | throw Error( 57 | 'Non-null separator required for SassList with more than one element.', 58 | ); 59 | } 60 | this.separatorInternal = 61 | options?.separator === undefined ? ',' : options.separator; 62 | this.hasBracketsInternal = options?.brackets ?? false; 63 | } 64 | 65 | get asList(): List { 66 | return this.contentsInternal; 67 | } 68 | 69 | /** Whether `this` has brackets. */ 70 | get hasBrackets(): boolean { 71 | return this.hasBracketsInternal; 72 | } 73 | 74 | /** `this`'s list separator. */ 75 | get separator(): ListSeparator { 76 | return this.separatorInternal; 77 | } 78 | 79 | protected get lengthAsList(): number { 80 | return this.contentsInternal.size; 81 | } 82 | 83 | get(index: number): Value | undefined { 84 | return this.contentsInternal.get(index); 85 | } 86 | 87 | assertList(): SassList { 88 | return this; 89 | } 90 | 91 | assertMap(name?: string): SassMap { 92 | if (this.contentsInternal.isEmpty()) return new SassMap(); 93 | throw valueError(`${this} is not a map`, name); 94 | } 95 | 96 | /** 97 | * If `this` is empty, returns an empty OrderedMap. 98 | * 99 | * Otherwise, returns null. 100 | */ 101 | tryMap(): SassMap | null { 102 | return this.contentsInternal.isEmpty() ? new SassMap() : null; 103 | } 104 | 105 | equals(other: Value): boolean { 106 | if ( 107 | (other instanceof SassList || other instanceof SassMap) && 108 | this.contentsInternal.isEmpty() && 109 | other.asList.isEmpty() 110 | ) { 111 | return true; 112 | } 113 | 114 | if ( 115 | !(other instanceof SassList) || 116 | this.hasBrackets !== other.hasBrackets || 117 | this.separator !== other.separator 118 | ) { 119 | return false; 120 | } 121 | 122 | return this.contentsInternal.equals(other.asList); 123 | } 124 | 125 | hashCode(): number { 126 | return this.contentsInternal.isEmpty() 127 | ? emptyListHashCode 128 | : this.contentsInternal.hashCode() ^ 129 | hash(this.hasBrackets) ^ 130 | hash(this.separator); 131 | } 132 | 133 | toString(): string { 134 | let string = ''; 135 | if (this.hasBrackets) string += '['; 136 | string += `${this.contentsInternal.join( 137 | this.separator === ' ' || this.separator === null 138 | ? ' ' 139 | : `${this.separator} `, 140 | )}`; 141 | if (this.hasBrackets) string += ']'; 142 | return string; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/value/map.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {List, OrderedMap} from 'immutable'; 6 | 7 | import {Value} from './index'; 8 | import {ListSeparator, SassList} from './list'; 9 | 10 | /** A SassScript map */ 11 | export class SassMap extends Value { 12 | private readonly contentsInternal: OrderedMap; 13 | 14 | /** Returns a map that contains `contents`. */ 15 | constructor(contents?: OrderedMap) { 16 | super(); 17 | this.contentsInternal = contents ?? OrderedMap(); 18 | } 19 | 20 | /** The separator for `this`'s contents as a list. */ 21 | get separator(): ListSeparator { 22 | return this.contentsInternal.isEmpty() ? null : ','; 23 | } 24 | 25 | /** `this`'s contents. */ 26 | get contents(): OrderedMap { 27 | return this.contentsInternal; 28 | } 29 | 30 | /** 31 | * Returns an immutable list of `contents`'s keys and values as two-element 32 | * `SassList`s. 33 | */ 34 | get asList(): List { 35 | const list = []; 36 | for (const entry of this.contents.entries()) { 37 | list.push(new SassList(entry, {separator: ' '})); 38 | } 39 | return List(list); 40 | } 41 | 42 | protected get lengthAsList(): number { 43 | return this.contentsInternal.size; 44 | } 45 | 46 | get(indexOrKey: number | Value): Value | undefined { 47 | if (indexOrKey instanceof Value) { 48 | return this.contentsInternal.get(indexOrKey); 49 | } else { 50 | const entry = this.contentsInternal 51 | .entrySeq() 52 | .get(Math.floor(indexOrKey)); 53 | return entry ? new SassList(entry, {separator: ' '}) : undefined; 54 | } 55 | } 56 | 57 | assertMap(): SassMap { 58 | return this; 59 | } 60 | 61 | tryMap(): SassMap { 62 | return this; 63 | } 64 | 65 | equals(other: Value): boolean { 66 | if ( 67 | other instanceof SassList && 68 | this.contents.size === 0 && 69 | other.asList.size === 0 70 | ) { 71 | return true; 72 | } 73 | 74 | if ( 75 | !(other instanceof SassMap) || 76 | this.contents.size !== other.contents.size 77 | ) { 78 | return false; 79 | } 80 | 81 | for (const [key, value] of this.contents.entries()) { 82 | const otherValue = other.contents.get(key); 83 | if (otherValue === undefined || !otherValue.equals(value)) { 84 | return false; 85 | } 86 | } 87 | return true; 88 | } 89 | 90 | hashCode(): number { 91 | return this.contents.isEmpty() 92 | ? new SassList().hashCode() 93 | : // SassMaps with the same key-value pairs are considered equal 94 | // regardless of key-value order, so this hash must be order 95 | // independent. Since OrderedMap.hashCode() encodes the key-value order, 96 | // we use a manual XOR accumulator instead. 97 | this.contents.reduce( 98 | (accumulator, value, key) => 99 | accumulator ^ value.hashCode() ^ key.hashCode(), 100 | 0, 101 | ); 102 | } 103 | 104 | toString(): string { 105 | let string = '('; 106 | string += Array.from( 107 | this.contents.entries(), 108 | ([key, value]) => `${key}: ${value}`, 109 | ).join(', '); 110 | string += ')'; 111 | return string; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/src/value/mixin.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {hash} from 'immutable'; 6 | 7 | import {Value} from './index'; 8 | 9 | /** A first-class SassScript mixin. */ 10 | export class SassMixin extends Value { 11 | /** 12 | * This is the unique ID that the compiler uses to determine which mixin it 13 | * refers to. 14 | * 15 | * This is marked as public so that the protofier can access it, but it's not 16 | * part of the package's public API and should not be accessed by user code. 17 | * It may be renamed or removed without warning in the future. 18 | */ 19 | readonly id: number; 20 | 21 | /** 22 | * This is the unique context that the host uses to determine which 23 | * compilation this mixin belongs to. 24 | * 25 | * This is marked as public so that the protofier can access it, but it's not 26 | * part of the package's public API and should not be accessed by user code. 27 | * It may be renamed or removed without warning in the future. 28 | */ 29 | readonly compileContext: symbol; 30 | 31 | constructor(id: number, compileContext: symbol) { 32 | super(); 33 | this.id = id; 34 | this.compileContext = compileContext; 35 | } 36 | 37 | equals(other: Value): boolean { 38 | return ( 39 | other instanceof SassMixin && 40 | other.compileContext === this.compileContext && 41 | other.id === this.id 42 | ); 43 | } 44 | 45 | hashCode(): number { 46 | return hash(this.id); 47 | } 48 | 49 | toString(): string { 50 | return ``; 51 | } 52 | 53 | assertMixin(): SassMixin { 54 | return this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/value/null.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {hash} from 'immutable'; 6 | 7 | import {Value} from './index'; 8 | 9 | const hashCode = hash(null); 10 | 11 | // SassScript null. Cannot be constructed; exists only as the exported 12 | // singleton. 13 | export class SassNull extends Value { 14 | // Whether callers are allowed to construct this class. This is set to 15 | // `false` once the two constants are constructed so that the constructor 16 | // throws an error for future calls, in accordance with the legacy API. 17 | static constructionAllowed = true; 18 | 19 | constructor() { 20 | super(); 21 | 22 | if (!SassNull.constructionAllowed) { 23 | throw ( 24 | "new sass.types.Null() isn't allowed.\n" + 25 | 'Use sass.types.Null.NULL instead.' 26 | ); 27 | } 28 | 29 | Object.freeze(this); 30 | } 31 | 32 | get isTruthy(): boolean { 33 | return false; 34 | } 35 | 36 | get realNull(): null { 37 | return null; 38 | } 39 | 40 | equals(other: Value): boolean { 41 | return this === other; 42 | } 43 | 44 | hashCode(): number { 45 | return hashCode; 46 | } 47 | 48 | toString(): string { 49 | return 'sassNull'; 50 | } 51 | 52 | // Legacy API support 53 | static NULL: SassNull; 54 | } 55 | 56 | /** The singleton instance of SassScript null. */ 57 | export const sassNull = new SassNull(); 58 | 59 | // Legacy API support 60 | SassNull.constructionAllowed = false; 61 | 62 | SassNull.NULL = sassNull; 63 | -------------------------------------------------------------------------------- /lib/src/value/string.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {hash} from 'immutable'; 6 | 7 | import {Value} from './index'; 8 | import {valueError} from '../utils'; 9 | 10 | /** A SassScript string. */ 11 | export class SassString extends Value { 12 | private readonly textInternal: string; 13 | private readonly hasQuotesInternal: boolean; 14 | 15 | /** Creates a string with `text`, optionally with quotes. */ 16 | constructor(text: string, options?: {quotes?: boolean}); 17 | constructor(options?: {quotes?: boolean}); 18 | constructor( 19 | textOrOptions?: string | {quotes?: boolean}, 20 | options?: {quotes?: boolean}, 21 | ) { 22 | super(); 23 | 24 | if (typeof textOrOptions === 'string') { 25 | this.textInternal = textOrOptions; 26 | this.hasQuotesInternal = options?.quotes ?? true; 27 | } else { 28 | this.textInternal = ''; 29 | this.hasQuotesInternal = textOrOptions?.quotes ?? true; 30 | } 31 | } 32 | 33 | /** Creates an empty string, optionally with quotes. */ 34 | static empty(options?: {/** @default true */ quotes?: boolean}): SassString { 35 | return options === undefined || options?.quotes 36 | ? emptyQuoted 37 | : emptyUnquoted; 38 | } 39 | 40 | /** `this`'s text. */ 41 | get text(): string { 42 | return this.textInternal; 43 | } 44 | 45 | /** Whether `this` has quotes. */ 46 | get hasQuotes(): boolean { 47 | return this.hasQuotesInternal; 48 | } 49 | 50 | assertString(): SassString { 51 | return this; 52 | } 53 | 54 | /** 55 | * Sass's notion of `this`'s length. 56 | * 57 | * Sass treats strings as a series of Unicode code points while JS treats them 58 | * as a series of UTF-16 code units. For example, the character U+1F60A, 59 | * Smiling Face With Smiling Eyes, is a single Unicode code point but is 60 | * represented in UTF-16 as two code units (`0xD83D` and `0xDE0A`). So in 61 | * JS, `"n😊b".length` returns `4`, whereas in Sass `string.length("n😊b")` 62 | * returns `3`. 63 | */ 64 | get sassLength(): number { 65 | let length = 0; 66 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 67 | for (const codepoint of this.text) { 68 | length++; 69 | } 70 | return length; 71 | } 72 | 73 | /** 74 | * Converts `sassIndex` to a JS index into `text`. 75 | * 76 | * Sass indices are one-based, while JS indices are zero-based. Sass 77 | * indices may also be negative in order to index from the end of the string. 78 | * 79 | * In addition, Sass indices refer to Unicode code points while JS string 80 | * indices refer to UTF-16 code units. For example, the character U+1F60A, 81 | * Smiling Face With Smiling Eyes, is a single Unicode code point but is 82 | * represented in UTF-16 as two code units (`0xD83D` and `0xDE0A`). So in 83 | * JS, `"n😊b".charAt(1)` returns `0xD83D`, whereas in Sass 84 | * `string.slice("n😊b", 1, 1)` returns `"😊"`. 85 | * 86 | * This function converts Sass's code point indices to JS's code unit 87 | * indices. This means it's O(n) in the length of `text`. 88 | * 89 | * Throws an error `sassIndex` isn't a number, if that number isn't an 90 | * integer, or if that integer isn't a valid index for this string. 91 | * 92 | * If `sassIndex` came from a function argument, `name` is the argument name 93 | * (without the `$`) and is used for error reporting. 94 | */ 95 | sassIndexToStringIndex(sassIndex: Value, name?: string): number { 96 | let sassIdx = sassIndex.assertNumber().assertInt(); 97 | if (sassIdx === 0) { 98 | throw valueError('String index may not be 0', name); 99 | } 100 | 101 | const sassLength = this.sassLength; 102 | if (Math.abs(sassIdx) > sassLength) { 103 | throw valueError( 104 | `Invalid index ${sassIdx} for a string with ${sassLength} characters`, 105 | name, 106 | ); 107 | } 108 | if (sassIdx < 0) sassIdx += sassLength + 1; 109 | 110 | let pointer = 1; 111 | let idx = 0; 112 | for (const codePoint of this.text) { 113 | if (pointer === sassIdx) break; 114 | idx += codePoint.length; 115 | pointer++; 116 | } 117 | return idx; 118 | } 119 | 120 | equals(other: Value): boolean { 121 | return other instanceof SassString && this.text === other.text; 122 | } 123 | 124 | hashCode(): number { 125 | return hash(this.text); 126 | } 127 | 128 | toString(): string { 129 | return this.hasQuotes ? `"${this.text}"` : this.text; 130 | } 131 | } 132 | 133 | // A quoted empty string returned by `SassString.empty()`. 134 | const emptyQuoted = new SassString('', {quotes: true}); 135 | 136 | // An unquoted empty string returned by `SassString.empty()`. 137 | const emptyUnquoted = new SassString('', {quotes: false}); 138 | -------------------------------------------------------------------------------- /lib/src/value/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {hash} from 'immutable'; 6 | 7 | import {valueError} from '../utils'; 8 | 9 | /** The precision of Sass numbers. */ 10 | export const precision = 10; 11 | 12 | // The max distance two Sass numbers can be from each another before they're 13 | // considered different. 14 | // 15 | // Uses ** instead of Math.pow() for constant folding. 16 | const epsilon = 10 ** (-precision - 1); 17 | 18 | /** Whether `num1` and `num2` are equal within `epsilon`. */ 19 | export function fuzzyEquals(num1: number, num2: number): boolean { 20 | return Math.abs(num1 - num2) < epsilon; 21 | } 22 | 23 | /** 24 | * Returns a hash code for `num`. 25 | * 26 | * Two numbers that `fuzzyEquals` each other must have the same hash code. 27 | */ 28 | export function fuzzyHashCode(num: number): number { 29 | return !isFinite(num) || isNaN(num) 30 | ? hash(num) 31 | : hash(Math.round(num / epsilon)); 32 | } 33 | 34 | /** Whether `num1` < `num2`, within `epsilon`. */ 35 | export function fuzzyLessThan(num1: number, num2: number): boolean { 36 | return num1 < num2 && !fuzzyEquals(num1, num2); 37 | } 38 | 39 | /** Whether `num1` <= `num2`, within `epsilon`. */ 40 | export function fuzzyLessThanOrEquals(num1: number, num2: number): boolean { 41 | return num1 < num2 || fuzzyEquals(num1, num2); 42 | } 43 | 44 | /** Whether `num1` > `num2`, within `epsilon`. */ 45 | export function fuzzyGreaterThan(num1: number, num2: number): boolean { 46 | return num1 > num2 && !fuzzyEquals(num1, num2); 47 | } 48 | 49 | /** Whether `num1` >= `num2`, within `epsilon`. */ 50 | export function fuzzyGreaterThanOrEquals(num1: number, num2: number): boolean { 51 | return num1 > num2 || fuzzyEquals(num1, num2); 52 | } 53 | 54 | /** Whether `num` `fuzzyEquals` an integer. */ 55 | export function fuzzyIsInt(num: number): boolean { 56 | return !isFinite(num) || isNaN(num) 57 | ? false 58 | : // Check against 0.5 rather than 0.0 so that we catch numbers that are 59 | // both very slightly above an integer, and very slightly below. 60 | fuzzyEquals(Math.abs(num - 0.5) % 1, 0.5); 61 | } 62 | 63 | /** 64 | * If `num` `fuzzyIsInt`, returns it as an integer. Otherwise, returns `null`. 65 | */ 66 | export function fuzzyAsInt(num: number): number | null { 67 | return fuzzyIsInt(num) ? Math.round(num) : null; 68 | } 69 | 70 | /** 71 | * Rounds `num` to the nearest integer. 72 | * 73 | * If `num` `fuzzyEquals` `x.5`, rounds away from zero. 74 | */ 75 | export function fuzzyRound(num: number): number { 76 | if (num > 0) { 77 | return fuzzyLessThan(num % 1, 0.5) ? Math.floor(num) : Math.ceil(num); 78 | } else { 79 | return fuzzyGreaterThan(num % 1, -0.5) ? Math.ceil(num) : Math.floor(num); 80 | } 81 | } 82 | 83 | /** 84 | * Returns `num` if it's within `min` and `max`, or `null` if it's not. 85 | * 86 | * If `num` `fuzzyEquals` `min` or `max`, it gets clamped to that value. 87 | */ 88 | export function fuzzyInRange( 89 | num: number, 90 | min: number, 91 | max: number, 92 | ): number | null { 93 | if (fuzzyEquals(num, min)) return min; 94 | if (fuzzyEquals(num, max)) return max; 95 | if (num > min && num < max) return num; 96 | return null; 97 | } 98 | 99 | /** 100 | * Returns `num` if it's within `min` and `max`. Otherwise, throws an error. 101 | * 102 | * If `num` `fuzzyEquals` `min` or `max`, it gets clamped to that value. 103 | * 104 | * If `name` is provided, it is used as the parameter name for error reporting. 105 | */ 106 | export function fuzzyAssertInRange( 107 | num: number, 108 | min: number, 109 | max: number, 110 | name?: string, 111 | ): number { 112 | if (fuzzyEquals(num, min)) return min; 113 | if (fuzzyEquals(num, max)) return max; 114 | if (num > min && num < max) return num; 115 | throw valueError(`${num} must be between ${min} and ${max}`, name); 116 | } 117 | 118 | /** Returns `dividend % modulus`, but always in the range `[0, modulus)`. */ 119 | export function positiveMod(dividend: number, modulus: number): number { 120 | const result = dividend % modulus; 121 | return result < 0 ? result + modulus : result; 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/version.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as api from './vendor/sass'; 6 | 7 | export class Version implements api.Version { 8 | constructor( 9 | readonly major: number, 10 | readonly minor: number, 11 | readonly patch: number, 12 | ) {} 13 | static parse(version: string): Version { 14 | const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/); 15 | if (match === null) { 16 | throw new Error(`Invalid version ${version}`); 17 | } 18 | return new Version( 19 | parseInt(match[1]), 20 | parseInt(match[2]), 21 | parseInt(match[3]), 22 | ); 23 | } 24 | toString(): string { 25 | return `${this.major}.${this.minor}.${this.patch}`; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /npm/android-arm/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-android-arm` 2 | 3 | This is the **android-arm** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/android-arm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-android-arm", 3 | "version": "1.89.1", 4 | "description": "The android-arm binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "android" 16 | ], 17 | "cpu": [ 18 | "arm" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/android-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-android-arm64` 2 | 3 | This is the **android-arm64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/android-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-android-arm64", 3 | "version": "1.89.1", 4 | "description": "The android-arm64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "android" 16 | ], 17 | "cpu": [ 18 | "arm64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/android-riscv64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-android-riscv64` 2 | 3 | This is the **android-riscv64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/android-riscv64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-android-riscv64", 3 | "version": "1.89.1", 4 | "description": "The android-riscv64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "android" 16 | ], 17 | "cpu": [ 18 | "riscv64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/android-x64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-android-x64` 2 | 3 | This is the **android-x64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/android-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-android-x64", 3 | "version": "1.89.1", 4 | "description": "The android-x64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "android" 16 | ], 17 | "cpu": [ 18 | "x64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/darwin-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-darwin-arm64` 2 | 3 | This is the **darwin-arm64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/darwin-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-darwin-arm64", 3 | "version": "1.89.1", 4 | "description": "The darwin-arm64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "darwin" 16 | ], 17 | "cpu": [ 18 | "arm64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/darwin-x64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-darwin-x64` 2 | 3 | This is the **darwin-x64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/darwin-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-darwin-x64", 3 | "version": "1.89.1", 4 | "description": "The darwin-x64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "darwin" 16 | ], 17 | "cpu": [ 18 | "x64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-arm/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-arm` 2 | 3 | This is the **linux-arm** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-arm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-arm", 3 | "version": "1.89.1", 4 | "description": "The linux-arm binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "arm" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-arm64` 2 | 3 | This is the **linux-arm64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-arm64", 3 | "version": "1.89.1", 4 | "description": "The linux-arm64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "arm64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-musl-arm/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-musl-arm` 2 | 3 | This is the **linux-musl-arm** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-musl-arm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-musl-arm", 3 | "version": "1.89.1", 4 | "description": "The linux-musl-arm binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "arm" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-musl-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-musl-arm64` 2 | 3 | This is the **linux-musl-arm64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-musl-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-musl-arm64", 3 | "version": "1.89.1", 4 | "description": "The linux-musl-arm64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "arm64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-musl-riscv64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-musl-riscv64` 2 | 3 | This is the **linux-musl-riscv64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-musl-riscv64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-musl-riscv64", 3 | "version": "1.89.1", 4 | "description": "The linux-musl-riscv64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "riscv64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-musl-x64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-musl-x64` 2 | 3 | This is the **linux-musl-x64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-musl-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-musl-x64", 3 | "version": "1.89.1", 4 | "description": "The linux-musl-x64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "x64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-riscv64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-riscv64` 2 | 3 | This is the **linux-riscv64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-riscv64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-riscv64", 3 | "version": "1.89.1", 4 | "description": "The linux-riscv64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "riscv64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/linux-x64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-linux-x64` 2 | 3 | This is the **linux-x64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/linux-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-linux-x64", 3 | "version": "1.89.1", 4 | "description": "The linux-x64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "linux" 16 | ], 17 | "cpu": [ 18 | "x64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/win32-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-win32-arm64` 2 | 3 | This is the **win32-arm64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/win32-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-win32-arm64", 3 | "version": "1.89.1", 4 | "description": "The win32-arm64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "win32" 16 | ], 17 | "cpu": [ 18 | "arm64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /npm/win32-x64/README.md: -------------------------------------------------------------------------------- 1 | # `sass-embedded-win32-x64` 2 | 3 | This is the **win32-x64** binary for [`sass-embedded`](https://www.npmjs.com/package/sass-embedded) 4 | -------------------------------------------------------------------------------- /npm/win32-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded-win32-x64", 3 | "version": "1.89.1", 4 | "description": "The win32-x64 binary for sass-embedded", 5 | "repository": "sass/embedded-host-node", 6 | "author": "Google Inc.", 7 | "license": "MIT", 8 | "files": [ 9 | "dart-sass/**/*" 10 | ], 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "os": [ 15 | "win32" 16 | ], 17 | "cpu": [ 18 | "x64" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-embedded", 3 | "version": "1.89.1", 4 | "protocol-version": "3.2.0", 5 | "compiler-version": "1.89.1", 6 | "description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol", 7 | "repository": "sass/embedded-host-node", 8 | "author": "Google Inc.", 9 | "license": "MIT", 10 | "exports": { 11 | "import": { 12 | "types": "./dist/types/index.m.d.ts", 13 | "default": "./dist/lib/index.mjs" 14 | }, 15 | "types": "./dist/types/index.d.ts", 16 | "default": "./dist/lib/index.js" 17 | }, 18 | "main": "dist/lib/index.js", 19 | "types": "dist/types/index.d.ts", 20 | "files": [ 21 | "dist/**/*" 22 | ], 23 | "engines": { 24 | "node": ">=16.0.0" 25 | }, 26 | "bin": { 27 | "sass": "dist/bin/sass.js" 28 | }, 29 | "scripts": { 30 | "init": "ts-node ./tool/init.ts", 31 | "check": "npm-run-all check:gts check:tsc", 32 | "check:gts": "gts check", 33 | "check:tsc": "tsc --noEmit", 34 | "clean": "gts clean", 35 | "compile": "tsc -p tsconfig.build.json && cp lib/index.mjs dist/lib/index.mjs && cp -r lib/src/vendor/sass/ dist/lib/src/vendor/sass && cp dist/lib/src/vendor/sass/index.d.ts dist/lib/src/vendor/sass/index.m.d.ts", 36 | "fix": "gts fix", 37 | "prepublishOnly": "npm run clean && ts-node ./tool/prepare-release.ts", 38 | "test": "jest" 39 | }, 40 | "optionalDependencies": { 41 | "sass-embedded-android-arm": "1.89.1", 42 | "sass-embedded-android-arm64": "1.89.1", 43 | "sass-embedded-android-riscv64": "1.89.1", 44 | "sass-embedded-android-x64": "1.89.1", 45 | "sass-embedded-darwin-arm64": "1.89.1", 46 | "sass-embedded-darwin-x64": "1.89.1", 47 | "sass-embedded-linux-arm": "1.89.1", 48 | "sass-embedded-linux-arm64": "1.89.1", 49 | "sass-embedded-linux-riscv64": "1.89.1", 50 | "sass-embedded-linux-x64": "1.89.1", 51 | "sass-embedded-linux-musl-arm": "1.89.1", 52 | "sass-embedded-linux-musl-arm64": "1.89.1", 53 | "sass-embedded-linux-musl-riscv64": "1.89.1", 54 | "sass-embedded-linux-musl-x64": "1.89.1", 55 | "sass-embedded-win32-arm64": "1.89.1", 56 | "sass-embedded-win32-x64": "1.89.1" 57 | }, 58 | "dependencies": { 59 | "@bufbuild/protobuf": "^2.0.0", 60 | "buffer-builder": "^0.2.0", 61 | "colorjs.io": "^0.5.0", 62 | "immutable": "^5.0.2", 63 | "rxjs": "^7.4.0", 64 | "supports-color": "^8.1.1", 65 | "sync-child-process": "^1.0.2", 66 | "varint": "^6.0.0" 67 | }, 68 | "devDependencies": { 69 | "@bufbuild/buf": "^1.39.0", 70 | "@bufbuild/protoc-gen-es": "^2.0.0", 71 | "@types/buffer-builder": "^0.2.0", 72 | "@types/google-protobuf": "^3.7.2", 73 | "@types/jest": "^29.4.0", 74 | "@types/node": "^22.0.0", 75 | "@types/shelljs": "^0.8.8", 76 | "@types/supports-color": "^8.1.1", 77 | "@types/tar": "^6.1.0", 78 | "@types/varint": "^6.0.1", 79 | "@types/yargs": "^17.0.4", 80 | "extract-zip": "^2.0.1", 81 | "gts": "^6.0.2", 82 | "jest": "^29.4.1", 83 | "npm-run-all": "^4.1.5", 84 | "shelljs": "^0.10.0", 85 | "simple-git": "^3.15.1", 86 | "source-map-js": "^1.0.2", 87 | "tar": "^7.4.3", 88 | "ts-jest": "^29.0.5", 89 | "ts-node": "^10.2.1", 90 | "typescript": "^5.0.2", 91 | "yaml": "^2.2.1", 92 | "yargs": "^17.2.1" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/after-compile-test.mjs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as fs from 'fs'; 6 | 7 | // Note: this file isn't .test.ts specifically because we _don't_ want Jest to 8 | // handle it, because Jest chokes on dynamic imports of literal ESM modules. 9 | 10 | // This file should only be run _after_ `npm run compile`. 11 | if (!fs.existsSync('dist/package.json')) { 12 | throw new Error('after-compile.test.ts must be run after `npm run compile`.'); 13 | } 14 | 15 | // Load these dynamically so we have a better error mesage if `npm run compile` 16 | // hasn't been run. 17 | const cjs = await import('../dist/lib/index.js'); 18 | const esm = await import('../dist/lib/index.mjs'); 19 | 20 | for (const [name, value] of Object.entries(cjs)) { 21 | if (name === '__esModule' || name === 'default') continue; 22 | if (!esm[name]) { 23 | throw new Error(`ESM module is missing export ${name}.`); 24 | } else if (esm[name] !== value) { 25 | throw new Error(`ESM ${name} isn't the same as CJS.`); 26 | } 27 | 28 | if (!esm.default[name]) { 29 | throw new Error(`ESM default export is missing export ${name}.`); 30 | } else if (esm.default[name] !== value) { 31 | throw new Error(`ESM default export ${name} isn't the same as CJS.`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/dependencies.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as fs from 'fs'; 6 | import * as p from 'path'; 7 | 8 | import * as pkg from '../package.json'; 9 | 10 | // These tests assert that our declared dependency on the embedded protocol is 11 | // either a -dev version or the same version we're testing against. 12 | 13 | it('declares a compatible dependency on the embedded protocol', () => { 14 | if (pkg['protocol-version'].endsWith('-dev')) return; 15 | 16 | expect( 17 | fs 18 | .readFileSync( 19 | p.join(__dirname, '../build/sass/spec/EMBEDDED_PROTOCOL_VERSION'), 20 | 'utf-8', 21 | ) 22 | .trim(), 23 | ).toBe(pkg['protocol-version']); 24 | }); 25 | -------------------------------------------------------------------------------- /test/sandbox.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as fs from 'fs'; 6 | import * as p from 'path'; 7 | 8 | import {PromiseOr} from '../lib/src/utils'; 9 | 10 | /** 11 | * Runs `test` within a sandbox directory. This directory is made available via 12 | * the `dir` function, which acts like `p.join()` but includes the sandbox 13 | * directory at the beginning. 14 | * 15 | * Handles all buildup and teardown. Returns a promise that resolves when `test` 16 | * finishes running. 17 | */ 18 | export async function run( 19 | test: (dir: (...paths: string[]) => string) => PromiseOr, 20 | options?: { 21 | // Directories to put in the SASS_PATH env variable before running test. 22 | sassPathDirs?: string[]; 23 | }, 24 | ): Promise { 25 | const testDir = p.join( 26 | p.dirname(__filename), 27 | 'sandbox', 28 | `${Math.random()}`.slice(2), 29 | ); 30 | fs.mkdirSync(testDir, {recursive: true}); 31 | if (options?.sassPathDirs) { 32 | process.env.SASS_PATH = options.sassPathDirs.join( 33 | process.platform === 'win32' ? ';' : ':', 34 | ); 35 | } 36 | try { 37 | await test((...paths) => p.join(testDir, ...paths)); 38 | } finally { 39 | if (options?.sassPathDirs) process.env.SASS_PATH = undefined; 40 | 41 | fs.rmSync(testDir, {force: true, recursive: true, maxRetries: 3}); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {Observable} from 'rxjs'; 6 | import {Value} from '../lib/src/value'; 7 | 8 | /** 9 | * Subscribes to `observable` and asserts that it errors with the expected 10 | * `errorMessage`. Calls `done()` to complete the spec. 11 | */ 12 | export function expectObservableToError( 13 | observable: Observable, 14 | errorMessage: string, 15 | done: () => void, 16 | ): void { 17 | observable.subscribe({ 18 | next: () => { 19 | throw new Error('expected error'); 20 | }, 21 | error: error => { 22 | expect(error.message).toBe(errorMessage); 23 | done(); 24 | }, 25 | complete: () => { 26 | throw new Error('expected error'); 27 | }, 28 | }); 29 | } 30 | 31 | /** 32 | * Asserts that the `actual` path is equal to the `expected` one, accounting for 33 | * OS differences. 34 | */ 35 | export function expectEqualPaths(actual: string, expected: string): void { 36 | if (process.platform === 'win32') { 37 | expect(actual.toLowerCase()).toBe(expected.toLowerCase()); 38 | } else { 39 | expect(actual).toBe(expected); 40 | } 41 | } 42 | 43 | /** 44 | * Asserts that `string1` is equal to `string2`, ignoring all whitespace in 45 | * either string. 46 | */ 47 | export function expectEqualIgnoringWhitespace( 48 | string1: string, 49 | string2: string, 50 | ): void { 51 | function strip(str: string): string { 52 | return str.replace(/\s+/g, ''); 53 | } 54 | expect(strip(string1)).toBe(strip(string2)); 55 | } 56 | 57 | /** 58 | * Asserts that `val1` and `val2` are equal and have the same hashcode. 59 | */ 60 | export function expectEqualWithHashCode(val1: Value, val2: Value): void { 61 | expect(val1.equals(val2)).toBe(true); 62 | expect(val1.hashCode()).toBe(val2.hashCode()); 63 | } 64 | -------------------------------------------------------------------------------- /tool/get-deprecations.ts: -------------------------------------------------------------------------------- 1 | // Generates the list of deprecations from spec/deprecations.yaml in the 2 | // language repo. 3 | 4 | import * as fs from 'fs'; 5 | import {parse} from 'yaml'; 6 | 7 | interface YamlData { 8 | [key: string]: { 9 | description: string; 10 | 'dart-sass': { 11 | status: 'active' | 'future' | 'obsolete'; 12 | deprecated?: string; 13 | obsolete?: string; 14 | }; 15 | }; 16 | } 17 | 18 | const yamlFile = 'build/sass/spec/deprecations.yaml'; 19 | 20 | /** 21 | * Converts a version string in the form X.Y.Z to be code calling the Version 22 | * constructor, or null if the string is undefined. 23 | */ 24 | function toVersionCode(version: string | undefined): string { 25 | if (!version) return 'null'; 26 | const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/); 27 | if (match === null) { 28 | throw new Error(`Invalid version ${version}`); 29 | } 30 | return `new Version(${match[1]}, ${match[2]}, ${match[3]})`; 31 | } 32 | 33 | /** 34 | * Generates the list of deprecations based on the YAML file in the language 35 | * repo. 36 | */ 37 | export async function getDeprecations(outDirectory: string): Promise { 38 | const yamlText = fs.readFileSync(yamlFile, 'utf8'); 39 | 40 | const deprecations = parse(yamlText) as YamlData; 41 | let tsText = 42 | "import {Deprecations} from './sass';\n" + 43 | "import {Version} from '../version';\n\n" + 44 | 'export const deprecations: Deprecations = {\n'; 45 | for (const [id, deprecation] of Object.entries(deprecations)) { 46 | const key = id.includes('-') ? `'${id}'` : id; 47 | const dartSass = deprecation['dart-sass']; 48 | tsText += 49 | ` ${key}: {\n` + 50 | ` id: '${id}',\n` + 51 | ` description: '${deprecation.description}',\n` + 52 | ` status: '${dartSass.status}',\n` + 53 | ` deprecatedIn: ${toVersionCode(dartSass.deprecated)},\n` + 54 | ` obsoleteIn: ${toVersionCode(dartSass.obsolete)},\n` + 55 | ' },\n'; 56 | } 57 | tsText += 58 | " 'user-authored': {\n" + 59 | " id: 'user-authored',\n" + 60 | " status: 'user',\n" + 61 | ' deprecatedIn: null,\n' + 62 | ' obsoleteIn: null,\n' + 63 | ' },\n' + 64 | '}\n'; 65 | 66 | fs.writeFileSync(`${outDirectory}/deprecations.ts`, tsText); 67 | } 68 | -------------------------------------------------------------------------------- /tool/get-embedded-compiler.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as p from 'path'; 6 | import * as shell from 'shelljs'; 7 | 8 | import * as utils from './utils'; 9 | 10 | /** 11 | * Downloads and builds the Embedded Dart Sass compiler. 12 | * 13 | * Can check out and build the source from a Git `ref` or build from the source 14 | * at `path`. By default, checks out the latest revision from GitHub. 15 | */ 16 | export async function getEmbeddedCompiler( 17 | outPath: string, 18 | options?: {ref: string} | {path: string}, 19 | ): Promise { 20 | const repo = 'dart-sass'; 21 | 22 | let source: string; 23 | if (!options || 'ref' in options) { 24 | utils.fetchRepo({ 25 | repo, 26 | outPath: 'build', 27 | ref: options?.ref ?? 'main', 28 | }); 29 | source = p.join('build', repo); 30 | } else { 31 | source = options.path; 32 | } 33 | 34 | // Make sure the compiler sees the same version of the language repo that the 35 | // host is using, but if they're already the same directory (as in the Dart 36 | // Sass CI environment) we don't need to do anything. 37 | const languageInHost = p.resolve('build/sass'); 38 | const languageInCompiler = p.resolve(p.join(source, 'build/language')); 39 | if (!(await utils.sameTarget(languageInHost, languageInCompiler))) { 40 | await utils.cleanDir(languageInCompiler); 41 | await utils.link(languageInHost, languageInCompiler); 42 | } 43 | 44 | buildDartSassEmbedded(source); 45 | await utils.link(p.join(source, 'build'), p.join(outPath, repo)); 46 | } 47 | 48 | // Builds the Embedded Dart Sass executable from the source at `repoPath`. 49 | function buildDartSassEmbedded(repoPath: string): void { 50 | console.log("Downloading Dart Sass's dependencies."); 51 | shell.exec('dart pub upgrade', { 52 | cwd: repoPath, 53 | silent: true, 54 | }); 55 | 56 | console.log('Building the Dart Sass executable.'); 57 | shell.exec('dart run grinder protobuf pkg-standalone-dev', { 58 | cwd: repoPath, 59 | env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'}, 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /tool/get-language-repo.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import * as p from 'path'; 6 | import * as shell from 'shelljs'; 7 | 8 | import * as utils from './utils'; 9 | 10 | /** 11 | * Downloads the Sass language repo and buids the Embedded Sass protocol 12 | * definition. 13 | * 14 | * Can check out and build the source from a Git `ref` or build from the source 15 | * at `path`. By default, checks out the latest revision from GitHub. 16 | */ 17 | export async function getLanguageRepo( 18 | outPath: string, 19 | options?: {ref: string} | {path: string}, 20 | ): Promise { 21 | if (!options || 'ref' in options) { 22 | utils.fetchRepo({ 23 | repo: 'sass', 24 | outPath: utils.BUILD_PATH, 25 | ref: options?.ref ?? 'main', 26 | }); 27 | } else { 28 | await utils.cleanDir('build/sass'); 29 | await utils.link(options.path, 'build/sass'); 30 | } 31 | 32 | // Workaround for https://github.com/shelljs/shelljs/issues/198 33 | // This file is a symlink which gets messed up by `shell.cp` (called from 34 | // `utils.link`) on Windows. 35 | if (process.platform === 'win32') shell.rm('build/sass/spec/README.md'); 36 | 37 | await utils.link('build/sass/js-api-doc', p.join(outPath, 'sass')); 38 | 39 | buildEmbeddedProtocol(); 40 | } 41 | 42 | // Builds the embedded proto into a TS file. 43 | function buildEmbeddedProtocol(): void { 44 | const version = shell.exec('npx buf --version', {silent: true}).stdout.trim(); 45 | console.log(`Building TS with buf ${version}.`); 46 | shell.exec('npx buf generate'); 47 | } 48 | -------------------------------------------------------------------------------- /tool/init.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import yargs from 'yargs'; 6 | 7 | import {getDeprecations} from './get-deprecations'; 8 | import {getEmbeddedCompiler} from './get-embedded-compiler'; 9 | import {getLanguageRepo} from './get-language-repo'; 10 | 11 | const argv = yargs(process.argv.slice(2)) 12 | .option('compiler-path', { 13 | type: 'string', 14 | description: 15 | 'Build the Embedded Dart Sass binary from the source at this path.', 16 | }) 17 | .option('compiler-ref', { 18 | type: 'string', 19 | description: 'Build the Embedded Dart Sass binary from this Git ref.', 20 | }) 21 | .option('skip-compiler', { 22 | type: 'boolean', 23 | description: "Don't Embedded Dart Sass at all.", 24 | }) 25 | .option('language-path', { 26 | type: 'string', 27 | description: 'Use the Sass language repo from the source at this path.', 28 | }) 29 | .option('language-ref', { 30 | type: 'string', 31 | description: 'Use the Sass language repo from this Git ref.', 32 | }) 33 | .conflicts({ 34 | 'compiler-path': ['compiler-ref', 'skip-compiler'], 35 | 'compiler-ref': ['skip-compiler'], 36 | 'language-path': ['language-ref'], 37 | }) 38 | .parseSync(); 39 | 40 | void (async () => { 41 | try { 42 | const outPath = 'lib/src/vendor'; 43 | 44 | if (argv['language-ref']) { 45 | await getLanguageRepo(outPath, { 46 | ref: argv['language-ref'], 47 | }); 48 | } else if (argv['language-path']) { 49 | await getLanguageRepo(outPath, { 50 | path: argv['language-path'], 51 | }); 52 | } else { 53 | await getLanguageRepo(outPath); 54 | } 55 | 56 | if (!argv['skip-compiler']) { 57 | if (argv['compiler-ref']) { 58 | await getEmbeddedCompiler(outPath, { 59 | ref: argv['compiler-ref'], 60 | }); 61 | } else if (argv['compiler-path']) { 62 | await getEmbeddedCompiler(outPath, { 63 | path: argv['compiler-path'], 64 | }); 65 | } else { 66 | await getEmbeddedCompiler(outPath); 67 | } 68 | } 69 | 70 | await getDeprecations(outPath); 71 | } catch (error) { 72 | console.error(error); 73 | process.exitCode = 1; 74 | } 75 | })(); 76 | -------------------------------------------------------------------------------- /tool/prepare-optional-release.ts: -------------------------------------------------------------------------------- 1 | import extractZip = require('extract-zip'); 2 | import {promises as fs} from 'fs'; 3 | import * as p from 'path'; 4 | import {extract as extractTar} from 'tar'; 5 | import yargs from 'yargs'; 6 | 7 | import * as pkg from '../package.json'; 8 | import * as utils from './utils'; 9 | 10 | export type DartPlatform = 11 | | 'android' 12 | | 'linux' 13 | | 'linux-musl' 14 | | 'macos' 15 | | 'windows'; 16 | export type DartArch = 'x64' | 'arm' | 'arm64' | 'riscv64'; 17 | 18 | const argv = yargs(process.argv.slice(2)) 19 | .option('package', { 20 | type: 'string', 21 | description: 22 | 'Directory name under `npm` directory that contains optional dependencies.', 23 | demandOption: true, 24 | choices: Object.keys(pkg.optionalDependencies).map( 25 | name => name.split('sass-embedded-')[1], 26 | ), 27 | }) 28 | .parseSync(); 29 | 30 | // Converts a Node-style platform name as returned by `process.platform` into a 31 | // name used by Dart Sass. Throws if the operating system is not supported by 32 | // Dart Sass Embedded. 33 | export function nodePlatformToDartPlatform(platform: string): DartPlatform { 34 | switch (platform) { 35 | case 'android': 36 | return 'android'; 37 | case 'linux': 38 | case 'linux-musl': 39 | return 'linux'; 40 | case 'darwin': 41 | return 'macos'; 42 | case 'win32': 43 | return 'windows'; 44 | default: 45 | throw Error(`Platform ${platform} is not supported.`); 46 | } 47 | } 48 | 49 | // Converts a Node-style architecture name as returned by `process.arch` into a 50 | // name used by Dart Sass. Throws if the architecture is not supported by Dart 51 | // Sass Embedded. 52 | export function nodeArchToDartArch(arch: string): DartArch { 53 | switch (arch) { 54 | case 'x64': 55 | return 'x64'; 56 | case 'arm': 57 | return 'arm'; 58 | case 'arm64': 59 | return 'arm64'; 60 | case 'riscv64': 61 | return 'riscv64'; 62 | default: 63 | throw Error(`Architecture ${arch} is not supported.`); 64 | } 65 | } 66 | 67 | // Get the platform's file extension for archives. 68 | function getArchiveExtension(platform: DartPlatform): '.zip' | '.tar.gz' { 69 | return platform === 'windows' ? '.zip' : '.tar.gz'; 70 | } 71 | 72 | // Downloads the release for `repo` located at `assetUrl`, then unzips it into 73 | // `outPath`. 74 | async function downloadRelease(options: { 75 | repo: string; 76 | assetUrl: string; 77 | outPath: string; 78 | }): Promise { 79 | console.log(`Downloading ${options.repo} release asset.`); 80 | const response = await fetch(options.assetUrl, { 81 | redirect: 'follow', 82 | }); 83 | if (!response.ok) { 84 | throw Error( 85 | `Failed to download ${options.repo} release asset: ${response.statusText}`, 86 | ); 87 | } 88 | const releaseAsset = Buffer.from(await response.arrayBuffer()); 89 | 90 | console.log(`Unzipping ${options.repo} release asset to ${options.outPath}.`); 91 | await utils.cleanDir(p.join(options.outPath, options.repo)); 92 | 93 | const archiveExtension = options.assetUrl.endsWith('.zip') 94 | ? '.zip' 95 | : '.tar.gz'; 96 | const zippedAssetPath = 97 | options.outPath + '/' + options.repo + archiveExtension; 98 | await fs.writeFile(zippedAssetPath, releaseAsset); 99 | if (archiveExtension === '.zip') { 100 | await extractZip(zippedAssetPath, { 101 | dir: p.join(process.cwd(), options.outPath), 102 | }); 103 | } else { 104 | extractTar({ 105 | file: zippedAssetPath, 106 | cwd: options.outPath, 107 | sync: true, 108 | }); 109 | } 110 | await fs.unlink(zippedAssetPath); 111 | } 112 | 113 | void (async () => { 114 | try { 115 | const version = pkg['compiler-version'] as string; 116 | if (version.endsWith('-dev')) { 117 | throw Error( 118 | "Can't release optional packages for a -dev compiler version.", 119 | ); 120 | } 121 | 122 | const index = argv.package.lastIndexOf('-'); 123 | const nodePlatform = argv.package.substring(0, index); 124 | const nodeArch = argv.package.substring(index + 1); 125 | const dartPlatform = nodePlatformToDartPlatform(nodePlatform); 126 | const dartArch = nodeArchToDartArch(nodeArch); 127 | const isMusl = nodePlatform === 'linux-musl'; 128 | const outPath = p.join('npm', argv.package); 129 | await downloadRelease({ 130 | repo: 'dart-sass', 131 | assetUrl: 132 | 'https://github.com/sass/dart-sass/releases/download/' + 133 | `${version}/dart-sass-${version}-` + 134 | `${dartPlatform}-${dartArch}${isMusl ? '-musl' : ''}` + 135 | `${getArchiveExtension(dartPlatform)}`, 136 | outPath, 137 | }); 138 | } catch (error) { 139 | console.error(error); 140 | process.exitCode = 1; 141 | } 142 | })(); 143 | -------------------------------------------------------------------------------- /tool/prepare-release.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {promises as fs} from 'fs'; 6 | import * as shell from 'shelljs'; 7 | 8 | import * as pkg from '../package.json'; 9 | import {getDeprecations} from './get-deprecations'; 10 | import {getLanguageRepo} from './get-language-repo'; 11 | 12 | void (async () => { 13 | try { 14 | await sanityCheckBeforeRelease(); 15 | 16 | await getLanguageRepo('lib/src/vendor'); 17 | 18 | await getDeprecations('lib/src/vendor'); 19 | 20 | console.log('Transpiling TS into dist.'); 21 | shell.exec('tsc -p tsconfig.build.json'); 22 | shell.cp('lib/index.mjs', 'dist/lib/index.mjs'); 23 | 24 | console.log('Copying JS API types to dist.'); 25 | shell.cp('-R', 'lib/src/vendor/sass', 'dist/types'); 26 | shell.cp('dist/types/index.d.ts', 'dist/types/index.m.d.ts'); 27 | await fs.unlink('dist/types/README.md'); 28 | 29 | console.log('Ready for publishing to npm.'); 30 | } catch (error) { 31 | console.error(error); 32 | process.exitCode = 1; 33 | } 34 | })(); 35 | 36 | // Quick sanity checks to make sure the release we are preparing is a suitable 37 | // candidate for release. 38 | async function sanityCheckBeforeRelease(): Promise { 39 | console.log('Running sanity checks before releasing.'); 40 | const releaseVersion = pkg.version; 41 | 42 | const ref = process.env['GITHUB_REF']; 43 | if (ref !== `refs/tags/${releaseVersion}`) { 44 | throw Error( 45 | `GITHUB_REF ${ref} is different than the package.json version ${releaseVersion}.`, 46 | ); 47 | } 48 | 49 | for (const [dep, version] of Object.entries(pkg.optionalDependencies)) { 50 | if (version !== releaseVersion) { 51 | throw Error( 52 | `optional dependency ${dep}'s version doesn't match ${releaseVersion}.`, 53 | ); 54 | } 55 | } 56 | 57 | if (releaseVersion.indexOf('-dev') > 0) { 58 | throw Error(`${releaseVersion} is a dev release.`); 59 | } 60 | 61 | const versionHeader = new RegExp(`^## ${releaseVersion}$`, 'm'); 62 | const changelog = await fs.readFile('CHANGELOG.md', 'utf8'); 63 | if (!changelog.match(versionHeader)) { 64 | throw Error(`There's no CHANGELOG entry for ${releaseVersion}.`); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tool/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. Use of this source code is governed by an 2 | // MIT-style license that can be found in the LICENSE file or at 3 | // https://opensource.org/licenses/MIT. 4 | 5 | import {existsSync, promises as fs, lstatSync} from 'fs'; 6 | import * as p from 'path'; 7 | import * as shell from 'shelljs'; 8 | 9 | shell.config.fatal = true; 10 | 11 | // Directory that holds source files. 12 | export const BUILD_PATH = 'build'; 13 | 14 | // Clones `repo` into `outPath`, then checks out the given Git `ref`. 15 | export function fetchRepo(options: { 16 | repo: string; 17 | outPath: string; 18 | ref: string; 19 | }): void { 20 | const path = p.join(options.outPath, options.repo); 21 | if (existsSync(p.join(path, '.git')) && lstatSync(path).isSymbolicLink()) { 22 | throw ( 23 | `${path} is a symlink to a git repo, not overwriting.\n` + 24 | `Run "rm ${path}" and try again.` 25 | ); 26 | } 27 | 28 | if (!existsSync(path)) { 29 | console.log(`Cloning ${options.repo} into ${options.outPath}.`); 30 | shell.exec( 31 | `git clone \ 32 | --depth=1 \ 33 | https://github.com/sass/${options.repo} \ 34 | ${path}`, 35 | ); 36 | } 37 | 38 | const version = 39 | options.ref === 'main' ? 'latest update' : `commit ${options.ref}`; 40 | console.log(`Fetching ${version} for ${options.repo}.`); 41 | shell.exec( 42 | `git fetch --depth=1 origin ${options.ref} && git reset --hard FETCH_HEAD`, 43 | {cwd: path}, 44 | ); 45 | } 46 | 47 | // Links or copies the contents of `source` into `destination`. 48 | export async function link(source: string, destination: string): Promise { 49 | await cleanDir(destination); 50 | if (process.platform === 'win32') { 51 | console.log(`Copying ${source} into ${destination}.`); 52 | shell.cp('-R', source, destination); 53 | } else { 54 | source = p.resolve(source); 55 | console.log(`Linking ${source} into ${destination}.`); 56 | // Symlinking doesn't play nice with Jasmine's test globbing on Windows. 57 | await fs.symlink(source, destination); 58 | } 59 | } 60 | 61 | // Ensures that `dir` does not exist, but its parent directory does. 62 | export async function cleanDir(dir: string): Promise { 63 | await fs.mkdir(p.dirname(dir), {recursive: true}); 64 | try { 65 | await fs.rm(dir, {force: true, recursive: true}); 66 | } catch (_) { 67 | // If dir doesn't exist yet, that's fine. 68 | } 69 | } 70 | 71 | // Returns whether [path1] and [path2] are symlinks that refer to the same file. 72 | export async function sameTarget( 73 | path1: string, 74 | path2: string, 75 | ): Promise { 76 | const realpath1 = await tryRealpath(path1); 77 | if (realpath1 === null) return false; 78 | 79 | return realpath1 === (await tryRealpath(path2)); 80 | } 81 | 82 | // Like `fs.realpath()`, but returns `null` if the path doesn't exist on disk. 83 | async function tryRealpath(path: string): Promise { 84 | try { 85 | return await fs.realpath(p.resolve(path)); 86 | } catch (_) { 87 | return null; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "jest.config.js", 5 | "lib/src/vendor/dart-sass/**", 6 | "**/*.test.ts" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "dist", 6 | "resolveJsonModule": true, 7 | "rootDir": ".", 8 | "useUnknownInCatchVariables": false, 9 | "declarationDir": "_types", 10 | "lib": ["DOM"] 11 | }, 12 | "include": [ 13 | "package.json", 14 | "*.ts", 15 | "bin/*.ts", 16 | "lib/**/*.ts", 17 | "tool/**/*.ts", 18 | "test/**/*.ts" 19 | ], 20 | "exclude": ["lib/src/vendor/dart-sass/**"] 21 | } 22 | --------------------------------------------------------------------------------