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