diff --git a/.eslintignore b/.eslintignore index 41fc7bf4..1505e1ca 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ build/ dist/ language/ -embedded-protocol/ lib/src/vendor/ **/*.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 311323b2..70129465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ defaults: env: PROTOC_VERSION: 3.x - DEFAULT_NODE_VERSION: 15.x # If changing this, also change jobs.tests.strategy.matrix.node_version + DEFAULT_NODE_VERSION: 18.x # If changing this, also change jobs.tests.strategy.matrix.node_version on: push: @@ -24,24 +24,15 @@ jobs: with: node-version: ${{ env.DEFAULT_NODE_VERSION }} check-latest: true - - uses: arduino/setup-protoc@v1 - with: - version: ${{ env.PROTOC_VERSION }} - repo-token: '${{ github.token }}' - - - name: Check out the embedded protocol - uses: sass/clone-linked-repo@v1 - with: {repo: sass/embedded-protocol, default-ref: null} - - name: Check out the JS API definition + - name: Check out the language repo uses: sass/clone-linked-repo@v1 with: {repo: sass/sass, path: language} - run: npm install - name: npm run init run: | - if [[ -d embedded-protocol ]]; then args=--protocol-path=embedded-protocol; fi - npm run init -- --skip-compiler --api-path=language $args + npm run init -- --skip-compiler --language-path=language $args - run: npm run check @@ -52,7 +43,7 @@ jobs: strategy: matrix: os: [ubuntu, macos, windows] - node-version: [15.x, 14.x, 12.x] # If changing this, also change env.DEFAULT_NODE_VERSION + node-version: [18.x, 16.x, 14.x] # If changing this, also change env.DEFAULT_NODE_VERSION fail-fast: false steps: @@ -61,44 +52,26 @@ jobs: with: node-version: ${{ matrix.node-version }} check-latest: true - - uses: frenck/action-setup-yq@v1 - with: {version: v4.30.5} # frenck/action-setup-yq#35 - - uses: arduino/setup-protoc@v1 - with: - version: ${{ env.PROTOC_VERSION }} - repo-token: '${{ github.token }}' - uses: dart-lang/setup-dart@v1 with: {sdk: stable} - run: dart --version - - name: Check out the embedded protocol - uses: sass/clone-linked-repo@v1 - with: {repo: sass/embedded-protocol, default-ref: null} - - name: Check out Dart Sass uses: sass/clone-linked-repo@v1 with: {repo: sass/dart-sass} - - name: Check out the embedded compiler - uses: sass/clone-linked-repo@v1 - with: {repo: sass/dart-sass-embedded} - - - name: Link the embedded compiler to Dart Sass - run: | - yq -i '.dependency_overrides.sass = {"path": "../dart-sass"}' \ - dart-sass-embedded/pubspec.yaml - - - name: Check out the JS API definition + - name: Check out the language repo uses: sass/clone-linked-repo@v1 with: {repo: sass/sass, path: language} - run: npm install - name: npm run init run: | - if [[ -d embedded-protocol ]]; then args=--protocol-path=embedded-protocol; fi - npm run init -- --compiler-path=dart-sass-embedded --api-path=language $args + npm run init -- --compiler-path=dart-sass --language-path=language $args - run: npm run test + - run: npm run compile + - run: node test/after-compile-test.mjs # The versions should be kept up-to-date with the latest LTS Node releases. # They next need to be rotated October 2021. See @@ -111,13 +84,13 @@ jobs: fail-fast: false matrix: os: [ubuntu, windows, macos] - node_version: [16] + node_version: [18] include: # Include LTS versions on Ubuntu - os: ubuntu - node_version: 14 + node_version: 16 - os: ubuntu - node_version: 12 + node_version: 14 steps: - uses: actions/checkout@v2 @@ -125,39 +98,19 @@ jobs: with: {sdk: stable} - uses: actions/setup-node@v2 with: {node-version: "${{ matrix.node_version }}"} - - uses: frenck/action-setup-yq@v1 - with: {version: v4.30.5} # frenck/action-setup-yq#35 - - uses: arduino/setup-protoc@v1 - with: - version: ${{ env.PROTOC_VERSION }} - repo-token: '${{ github.token }}' - - - name: Check out the embedded protocol - uses: sass/clone-linked-repo@v1 - with: {repo: sass/embedded-protocol, default-ref: null} - name: Check out Dart Sass uses: sass/clone-linked-repo@v1 with: {repo: sass/dart-sass} - - name: Check out the embedded compiler - uses: sass/clone-linked-repo@v1 - with: {repo: sass/dart-sass-embedded} - - - name: Link the embedded compiler to Dart Sass - run: | - yq -i '.dependency_overrides.sass = {"path": "../dart-sass"}' \ - dart-sass-embedded/pubspec.yaml - - - name: Check out the JS API definition + - name: Check out the language repo uses: sass/clone-linked-repo@v1 with: {repo: sass/sass, path: language} - run: npm install - name: npm run init run: | - if [[ -d embedded-protocol ]]; then args=--protocol-path=embedded-protocol; fi - npm run init -- --compiler-path=dart-sass-embedded --api-path=language $args + npm run init -- --compiler-path=dart-sass --language-path=language $args - name: Check out sass-spec uses: sass/clone-linked-repo@v1 @@ -170,7 +123,15 @@ jobs: - name: Compile run: | npm run compile - ln -s {`pwd`/,dist/}lib/src/vendor/dart-sass-embedded + if [[ "$RUNNER_OS" == "Windows" ]]; then + # Avoid copying the entire Dart Sass build directory on Windows, + # since it may contain symlinks that cp will choke on. + mkdir -p dist/lib/src/vendor/dart-sass/ + cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.bat + cp {`pwd`/,dist/}lib/src/vendor/dart-sass/sass.snapshot + else + ln -s {`pwd`/,dist/}lib/src/vendor/dart-sass + fi - name: Run tests run: npm run js-api-spec -- --sassPackage .. --sassSassRepo ../language diff --git a/.gitignore b/.gitignore index 1f8d93b7..cc631e31 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules npm-debug.log* package-lock.json test/sandbox -npm/*/dart-sass-embedded/ +npm/*/dart-sass/ # Editors .idea diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f530226..3c81a93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,271 @@ +## 1.64.2 + +### Dart API + +* Include protocol buffer definitions when uploading the `sass` package to pub. + +## 1.64.1 + +### Embedded Sass + +* Fix a bug where a valid `SassCalculation.clamp()` with less than 3 arguments + would throw an error. + +## 1.64.0 + +* Comments that appear before or between `@use` and `@forward` rules are now + emitted in source order as much as possible, instead of always being emitted + after the CSS of all module dependencies. + +* Fix a bug where an interpolation in a custom property name crashed if the file + was loaded by a `@use` nested in an `@import`. + +### JavaScript API + +* Add a new `SassCalculation` type that represents the calculation objects added + in Dart Sass 1.40.0. + +* Add `Value.assertCalculation()`, which returns the value if it's a + `SassCalculation` and throws an error otherwise. + +* Produce a better error message when an environment that supports some Node.js + APIs loads the browser entrypoint but attempts to access the filesystem. + +### Embedded Sass + +* Fix a bug where nested relative `@imports` failed to load when using the + deprecated functions `render` or `renderSync` and those relative imports were + loaded multiple times across different files. + +## 1.63.6 + +### JavaScript API + +* Fix `import sass from 'sass'` again after it was broken in the last release. + +### Embedded Sass + +* Fix the `exports` declaration in `package.json`. + +## 1.63.5 + +### JavaScript API + +* Fix a bug where loading the package through both CJS `require()` and ESM + `import` could crash on Node.js. + +### Embedded Sass + +* Fix a deadlock when running at high concurrency on 32-bit systems. + +* Fix a race condition where the embedded compiler could deadlock or crash if a + compilation ID was reused immediately after the compilation completed. + +## 1.63.4 + +### JavaScript API + +* Re-enable support for `import sass from 'sass'` when loading the package from + an ESM module in Node.js. However, this syntax is now deprecated; ESM users + should use `import * as sass from 'sass'` instead. + + On the browser and other ESM-only platforms, only `import * as sass from + 'sass'` is supported. + +* Properly export the legacy API values `TRUE`, `FALSE`, `NULL`, and `types` from + the ECMAScript module API. + +### Embedded Sass + +* Fix a race condition where closing standard input while requests are in-flight + could sometimes cause the process to hang rather than shutting down + gracefully. + +* Properly include the root stylesheet's URL in the set of loaded URLs when it + fails to parse. + +## 1.63.3 + +### JavaScript API + +* Fix loading Sass as an ECMAScript module on Node.js. + +## 1.63.2 + +* No user-visible changes. + +## 1.63.1 + +* No user-visible changes. + +## 1.63.0 + +### JavaScript API + +* Dart Sass's JS API now supports running in the browser. Further details and + instructions for use are in [the README](README.md#dart-sass-in-the-browser). + +### Embedded Sass + +* The Dart Sass embedded compiler is now included as part of the primary Dart + Sass distribution, rather than a separate executable. To use the embedded + compiler, just run `sass --embedded` from any Sass executable (other than the + pure JS executable). + + The Node.js embedded host will still be distributed as the `sass-embedded` + package on npm. The only change is that it will now provide direct access to a + `sass` executable with the same CLI as the `sass` package. + +* The Dart Sass embedded compiler now uses version 2.0.0 of the Sass embedded + protocol. See [the spec][embedded-protocol-spec] for a full description of the + protocol, and [the changelog][embedded-protocol-changelog] for a summary of + changes since version 1.2.0. + + [embedded-protocol-spec]: https://github.com/sass/sass/blob/main/spec/embedded-protocol.md + [embedded-protocol-changelog]: https://github.com/sass/sass/blob/main/EMBEDDED_PROTOCOL_CHANGELOG.md + +* The Dart Sass embedded compiler now runs multiple simultaneous compilations in + parallel, rather than serially. + +## 1.62.1 + +* Fix a bug where `:has(+ &)` and related constructs would drop the leading + combinator. + +## 1.62.0 + +* Deprecate the use of multiple `!global` or `!default` flags on the same + variable. This deprecation is named `duplicate-var-flags`. + +* Allow special numbers like `var()` or `calc()` in the global functions: + `grayscale()`, `invert()`, `saturate()`, and `opacity()`. These are also + native CSS `filter` functions. This is in addition to number values which were + already allowed. + +* Fix a cosmetic bug where an outer rule could be duplicated after nesting was + resolved, instead of re-using a shared rule. + +## 1.61.0 + +* **Potentially breaking change:** Drop support for End-of-Life Node.js 12. + +* Fix remaining cases for the performance regression introduced in 1.59.0. + +### Embedded Sass + +* The JS embedded host now loads files from the working directory when using the + legacy API. + +## 1.60.0 + +* Add support for the `pi`, `e`, `infinity`, `-infinity`, and `NaN` constants in + calculations. These will be interpreted as the corresponding numbers. + +* Add support for unknown constants in calculations. These will be interpreted + as unquoted strings. + +* Serialize numbers with value `infinity`, `-infinity`, and `NaN` to `calc()` + expressions rather than CSS-invalid identifiers. Numbers with complex units + still can't be serialized. + +## 1.59.3 + +* Fix a performance regression introduced in 1.59.0. + +* The NPM release of 1.59.0 dropped support for Node 12 without actually + indicating so in its pubspec. This release temporarily adds back support so + that the latest Sass version that declares it supports Node 12 actually does + so. However, Node 12 is now end-of-life, so we will drop support for it + properly in an upcoming release. + +## 1.59.2 + +* No user-visible changes. + +## 1.59.1 + +* No user-visible changes. + +## 1.59.0 + +### Command Line Interface + +* Added a new `--fatal-deprecation` flag that lets you treat a deprecation + warning as an error. You can pass an individual deprecation ID + (e.g. `slash-div`) or you can pass a Dart Sass version to treat all + deprecations initially emitted in that version or earlier as errors. + +* New `--future-deprecation` flag that lets you opt into warning for use of + certain features that will be deprecated in the future. At the moment, the + only option is `--future-deprecation=import`, which will emit warnings for + Sass `@import` rules, which are not yet deprecated, but will be in the future. + +### Dart API + +* New `Deprecation` enum, which contains the different current and future + deprecations used by the new CLI flags. + +* The `compile` methods now take in `fatalDeprecations` and `futureDeprecations` + parameters, which work similarly to the CLI flags. + +## 1.58.4 + +* Pull `@font-face` to the root rather than bubbling the style rule selector + inwards. + +* Improve error messages for invalid CSS values passed to plain CSS functions. + +* Improve error messages involving selectors. + +### Embedded Sass + +* Improve the performance of starting up a compilation. + +## 1.58.3 + +* No user-visible changes. + +## 1.58.2 + +### Command Line Interface + +* Add a timestamp to messages printed in `--watch` mode. + +* Print better `calc()`-based suggestions for `/`-as-division expression that + contain calculation-incompatible constructs like unary minus. + +## 1.58.1 + +* Emit a unitless hue when serializing `hsl()` colors. The `deg` unit is + incompatible with IE, and while that officially falls outside our + compatibility policy, it's better to lean towards greater compatibility. + +## 1.58.0 + +* Remove sourcemap comments from Sass sources. The generated sourcemap comment + for the compiled CSS output remains unaffected. + +* Fix a bug in `@extend` logic where certain selectors with three or more + combinators were incorrectly considered superselectors of similar selectors + with fewer combinators, causing them to be incorrectly trimmed from the + output. + +* Produce a better error message for a number with a leading `+` or `-`, a + decimal point, but no digits. + +* Produce a better error message for a nested property whose name starts with + `--`. + +* Fix a crash when a selector ends in an escaped backslash. + +* Add the relative length units from CSS Values 4 and CSS Contain 3 as known + units to validate bad computation in `calc`. + +### Command Line Interface + +* The `--watch` flag will now track loads through calls to `meta.load-css()` as + long as their URLs are literal strings without any interpolation. + ## 1.57.1 * No user-visible changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20e63a3a..e7c61af0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,14 +41,12 @@ such, manual commits should never: The embedded host depends on several different components which come from different repositories: -* The [Dart Sass embedded compiler]. -* [Dart Sass] (transitively through the embedded compiler). +* The [Dart Sass compiler]. * The [Sass embedded protocol]. * The [Sass JS API definition]. -[Dart Sass embedded compiler]: https://github.com/sass/dart-sass-embedded -[Dart Sass]: https://github.com/sass/dart-sass -[Sass embedded protocol]: https://github.com/sass/embedded-protocol +[Dart Sass compiler]: https://github.com/sass/dart-sass +[Sass embedded protocol]: https://github.com/sass/sass/tree/main/spec/embedded-protocol.md [JS API definition]: https://github.com/sass/sass/tree/main/spec/js-api These dependencies are made available in different ways depending on context. @@ -77,15 +75,14 @@ By default: revision on GitHub. * This uses the Dart Sass version from the latest revision on GitHub, unless the - embedded `--compiler-path` was passed in which case it uses whatever version - of Dart Sass that package references. + `--compiler-path` was passed in which case it uses that version of Dart Sass. ## Continuous Integration CI tests also use `npm run init`, so they use the same defaults as local development. However, if the pull request description includes a link to a pull -request for the embedded compiler, Dart Sass, the embedded protocol, or the JS -API, this will check out that version and run tests against it instead. +request for Dart Sass, the embedded protocol, or the JS API, this will check out +that version and run tests against it instead. ## Release @@ -93,11 +90,11 @@ When this package is released to npm, it downloads the embedded protocol version that matches `protocol-version` in `package.json`. It downloads the latest JS API revision on GitHub. -The release version of the `sass-embedded` package does *not* include the -embedded compiler or Dart Sass. Instead, we release optional packages of the -form `sass-embedded--`. Each of these contains the published version -of the embedded compiler that matches `compiler-version` in `package.json` for -the given operating system/architecture combination. +The release version of the `sass-embedded` package does *not* include Dart Sass. +Instead, we release optional packages of the form `sass-embedded--`. +Each of these contains the published version of Dart Sass that matches +`compiler-version` in `package.json` for the given operating system/architecture +combination. If either `protocol-version` or `compiler-version` ends with `-dev`, the release will fail. diff --git a/README.md b/README.md index 7a1681b1..5cbc44b8 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,8 @@ to start a Sass compilation, but to control aspects of it that are exposed by an API. This includes defining custom importers, functions, and loggers, all of which are invoked by messages from the embedded compiler back to the host. -[embedded compiler]: https://github.com/sass/dart-sass-embedded -[Embedded Sass Protocol]: https://github.com/sass/embedded-protocol#readme +[embedded compiler]: https://github.com/sass/dart-sass#embedded-dart-sass +[Embedded Sass Protocol]: https://github.com/sass/sass/tree/main/spec/embedded-protocol.md Although this sort of two-way communication with an embedded process is inherently asynchronous in Node.js, this package supports the synchronous diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 00000000..e128634a --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,5 @@ +version: v1 +plugins: +- plugin: es + out: lib/src/vendor + opt: target=ts diff --git a/buf.work.yaml b/buf.work.yaml new file mode 100644 index 00000000..005eb6a0 --- /dev/null +++ b/buf.work.yaml @@ -0,0 +1,2 @@ +version: v1 +directories: [build/sass/spec] diff --git a/lib/index.mjs b/lib/index.mjs new file mode 100644 index 00000000..374e98e9 --- /dev/null +++ b/lib/index.mjs @@ -0,0 +1,174 @@ +import * as sass from './index.js'; + +export const compile = sass.compile; +export const compileAsync = sass.compileAsync; +export const compileString = sass.compileString; +export const compileStringAsync = sass.compileStringAsync; +export const Logger = sass.Logger; +export const CalculationInterpolation = sass.CalculationInterpolation +export const CalculationOperation = sass.CalculationOperation +export const CalculationOperator = sass.CalculationOperator +export const SassArgumentList = sass.SassArgumentList; +export const SassBoolean = sass.SassBoolean; +export const SassCalculation = sass.SassCalculation +export const SassColor = sass.SassColor; +export const SassFunction = sass.SassFunction; +export const SassList = sass.SassList; +export const SassMap = sass.SassMap; +export const SassNumber = sass.SassNumber; +export const SassString = sass.SassString; +export const Value = sass.Value; +export const CustomFunction = sass.CustomFunction; +export const ListSeparator = sass.ListSeparator; +export const sassFalse = sass.sassFalse; +export const sassNull = sass.sassNull; +export const sassTrue = sass.sassTrue; +export const Exception = sass.Exception; +export const PromiseOr = sass.PromiseOr; +export const info = sass.info; +export const render = sass.render; +export const renderSync = sass.renderSync; +export const TRUE = sass.TRUE; +export const FALSE = sass.FALSE; +export const NULL = sass.NULL; +export const types = sass.types; + +let printedDefaultExportDeprecation = false; +function defaultExportDeprecation() { + if (printedDefaultExportDeprecation) return; + printedDefaultExportDeprecation = true; + console.error( + "`import sass from 'sass'` is deprecated.\n" + + "Please use `import * as sass from 'sass'` instead."); +} + +export default { + get compile() { + defaultExportDeprecation(); + return sass.compile; + }, + get compileAsync() { + defaultExportDeprecation(); + return sass.compileAsync; + }, + get compileString() { + defaultExportDeprecation(); + return sass.compileString; + }, + get compileStringAsync() { + defaultExportDeprecation(); + return sass.compileStringAsync; + }, + get Logger() { + defaultExportDeprecation(); + return sass.Logger; + }, + get CalculationOperation() { + defaultExportDeprecation(); + return sass.CalculationOperation; + }, + get CalculationOperator() { + defaultExportDeprecation(); + return sass.CalculationOperator; + }, + get CalculationInterpolation() { + defaultExportDeprecation(); + return sass.CalculationInterpolation; + }, + get SassArgumentList() { + defaultExportDeprecation(); + return sass.SassArgumentList; + }, + get SassBoolean() { + defaultExportDeprecation(); + return sass.SassBoolean; + }, + get SassCalculation() { + defaultExportDeprecation(); + return sass.SassCalculation; + }, + get SassColor() { + defaultExportDeprecation(); + return sass.SassColor; + }, + get SassFunction() { + defaultExportDeprecation(); + return sass.SassFunction; + }, + get SassList() { + defaultExportDeprecation(); + return sass.SassList; + }, + get SassMap() { + defaultExportDeprecation(); + return sass.SassMap; + }, + get SassNumber() { + defaultExportDeprecation(); + return sass.SassNumber; + }, + get SassString() { + defaultExportDeprecation(); + return sass.SassString; + }, + get Value() { + defaultExportDeprecation(); + return sass.Value; + }, + get CustomFunction() { + defaultExportDeprecation(); + return sass.CustomFunction; + }, + get ListSeparator() { + defaultExportDeprecation(); + return sass.ListSeparator; + }, + get sassFalse() { + defaultExportDeprecation(); + return sass.sassFalse; + }, + get sassNull() { + defaultExportDeprecation(); + return sass.sassNull; + }, + get sassTrue() { + defaultExportDeprecation(); + return sass.sassTrue; + }, + get Exception() { + defaultExportDeprecation(); + return sass.Exception; + }, + get PromiseOr() { + defaultExportDeprecation(); + return sass.PromiseOr; + }, + get info() { + defaultExportDeprecation(); + return sass.info; + }, + get render() { + defaultExportDeprecation(); + return sass.render; + }, + get renderSync() { + defaultExportDeprecation(); + return sass.renderSync; + }, + get TRUE() { + defaultExportDeprecation(); + return sass.TRUE; + }, + get FALSE() { + defaultExportDeprecation(); + return sass.FALSE; + }, + get NULL() { + defaultExportDeprecation(); + return sass.NULL; + }, + get types() { + defaultExportDeprecation(); + return sass.types; + }, +}; diff --git a/lib/index.ts b/lib/index.ts index 3d75ea05..c79ffec2 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -16,6 +16,12 @@ export {SassNumber} from './src/value/number'; export {SassString} from './src/value/string'; export {Value} from './src/value'; export {sassNull} from './src/value/null'; +export { + CalculationOperation, + CalculationOperator, + CalculationInterpolation, + SassCalculation, +} from './src/value/calculations'; export * as types from './src/legacy/value'; export {Exception} from './src/exception'; diff --git a/lib/src/async-compiler.ts b/lib/src/async-compiler.ts index a78b0268..90aa0c88 100644 --- a/lib/src/async-compiler.ts +++ b/lib/src/async-compiler.ts @@ -6,7 +6,7 @@ import {spawn} from 'child_process'; import {Observable} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; -import {compilerPath} from './compiler-path'; +import {compilerCommand} from './compiler-path'; /** * An asynchronous wrapper for the embedded Sass compiler that exposes its stdio @@ -14,7 +14,11 @@ import {compilerPath} from './compiler-path'; */ export class AsyncEmbeddedCompiler { /** The underlying process that's being wrapped. */ - private readonly process = spawn(compilerPath, {windowsHide: true}); + private readonly process = spawn( + compilerCommand[0], + [...compilerCommand.slice(1), '--embedded'], + {windowsHide: true} + ); /** The child process's exit event. */ readonly exit$ = new Promise(resolve => { diff --git a/lib/src/compile.ts b/lib/src/compile.ts index 195bc447..c60cbf57 100644 --- a/lib/src/compile.ts +++ b/lib/src/compile.ts @@ -6,7 +6,7 @@ import * as p from 'path'; import {Observable} from 'rxjs'; import * as supportsColor from 'supports-color'; -import * as proto from './vendor/embedded-protocol/embedded_sass_pb'; +import * as proto from './vendor/embedded_sass_pb'; import * as utils from './utils'; import {AsyncEmbeddedCompiler} from './async-compiler'; import {CompileResult, Options, SourceSpan, StringOptions} from './vendor/sass'; @@ -73,9 +73,9 @@ function newCompilePathRequest( path: string, importers: ImporterRegistry<'sync' | 'async'>, options?: Options<'sync' | 'async'> -): proto.InboundMessage.CompileRequest { +): proto.InboundMessage_CompileRequest { const request = newCompileRequest(importers, options); - request.setPath(path); + request.input = {case: 'path', value: path}; return request; } @@ -84,29 +84,30 @@ function newCompileStringRequest( source: string, importers: ImporterRegistry<'sync' | 'async'>, options?: StringOptions<'sync' | 'async'> -): proto.InboundMessage.CompileRequest { - const input = new proto.InboundMessage.CompileRequest.StringInput(); - input.setSource(source); - input.setSyntax(utils.protofySyntax(options?.syntax ?? 'scss')); +): proto.InboundMessage_CompileRequest { + const input = new proto.InboundMessage_CompileRequest_StringInput({ + source, + syntax: utils.protofySyntax(options?.syntax ?? 'scss'), + }); const url = options?.url?.toString(); if (url && url !== legacyImporterProtocol) { - input.setUrl(url); + input.url = url; } if (options && 'importer' in options && options.importer) { - input.setImporter(importers.register(options.importer)); + input.importer = importers.register(options.importer); } else if (url === legacyImporterProtocol) { - const importer = new proto.InboundMessage.CompileRequest.Importer(); - importer.setPath(p.resolve('.')); - input.setImporter(importer); + input.importer = new proto.InboundMessage_CompileRequest_Importer({ + importer: {case: 'path', value: p.resolve('.')}, + }); } else { // When importer is not set on the host, the compiler will set a // FileSystemImporter if `url` is set to a file: url or a NoOpImporter. } const request = newCompileRequest(importers, options); - request.setString(input); + request.input = {case: 'string', value: input}; return request; } @@ -115,25 +116,26 @@ function newCompileStringRequest( function newCompileRequest( importers: ImporterRegistry<'sync' | 'async'>, options?: Options<'sync' | 'async'> -): proto.InboundMessage.CompileRequest { - const request = new proto.InboundMessage.CompileRequest(); - request.setImportersList(importers.importers); - request.setGlobalFunctionsList(Object.keys(options?.functions ?? {})); - request.setSourceMap(!!options?.sourceMap); - request.setSourceMapIncludeSources(!!options?.sourceMapIncludeSources); - request.setAlertColor(options?.alertColor ?? !!supportsColor.stdout); - request.setAlertAscii(!!options?.alertAscii); - request.setQuietDeps(!!options?.quietDeps); - request.setVerbose(!!options?.verbose); - request.setCharset(!!(options?.charset ?? true)); +): proto.InboundMessage_CompileRequest { + const request = new proto.InboundMessage_CompileRequest({ + importers: importers.importers, + globalFunctions: Object.keys(options?.functions ?? {}), + sourceMap: !!options?.sourceMap, + sourceMapIncludeSources: !!options?.sourceMapIncludeSources, + alertColor: options?.alertColor ?? !!supportsColor.stdout, + alertAscii: !!options?.alertAscii, + quietDeps: !!options?.quietDeps, + verbose: !!options?.verbose, + charset: !!(options?.charset ?? true), + }); switch (options?.style ?? 'expanded') { case 'expanded': - request.setStyle(proto.OutputStyle.EXPANDED); + request.style = proto.OutputStyle.EXPANDED; break; case 'compressed': - request.setStyle(proto.OutputStyle.COMPRESSED); + request.style = proto.OutputStyle.COMPRESSED; break; default: @@ -147,7 +149,7 @@ function newCompileRequest( // resolves with the CompileResult. Throws if there were any protocol or // compilation errors. Shuts down the compiler after compilation. async function compileRequestAsync( - request: proto.InboundMessage.CompileRequest, + request: proto.InboundMessage_CompileRequest, importers: ImporterRegistry<'async'>, options?: Options<'async'> ): Promise { @@ -172,7 +174,7 @@ async function compileRequestAsync( dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); return handleCompileResponse( - await new Promise( + await new Promise( (resolve, reject) => dispatcher.sendCompileRequest(request, (err, response) => { if (err) { @@ -193,7 +195,7 @@ async function compileRequestAsync( // resolves with the CompileResult. Throws if there were any protocol or // compilation errors. Shuts down the compiler after compilation. function compileRequestSync( - request: proto.InboundMessage.CompileRequest, + request: proto.InboundMessage_CompileRequest, importers: ImporterRegistry<'sync'>, options?: Options<'sync'> ): CompileResult { @@ -218,7 +220,7 @@ function compileRequestSync( dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); let error: unknown; - let response: proto.OutboundMessage.CompileResponse | undefined; + let response: proto.OutboundMessage_CompileResponse | undefined; dispatcher.sendCompileRequest(request, (error_, response_) => { if (error_) { error = error_; @@ -257,6 +259,11 @@ function createDispatcher( ); return new Dispatcher( + // Since we only use one compilation per process, we can get away with + // hardcoding a compilation ID. Once we support multiple concurrent + // compilations with the same process, we'll need to ensure each one uses a + // unique ID. + 1, messageTransformer.outboundMessages$, message => messageTransformer.writeInboundMessage(message), handlers @@ -266,33 +273,32 @@ function createDispatcher( /** Handles a log event according to `options`. */ function handleLogEvent( options: Options<'sync' | 'async'> | undefined, - event: proto.OutboundMessage.LogEvent + event: proto.OutboundMessage_LogEvent ): void { - if (event.getType() === proto.LogEventType.DEBUG) { + if (event.type === proto.LogEventType.DEBUG) { if (options?.logger?.debug) { - options.logger.debug(event.getMessage(), { - span: deprotofySourceSpan(event.getSpan()!), + options.logger.debug(event.message, { + span: deprotofySourceSpan(event.span!), }); } else { - console.error(event.getFormatted()); + console.error(event.formatted); } } else { if (options?.logger?.warn) { const params: {deprecation: boolean; span?: SourceSpan; stack?: string} = { - deprecation: - event.getType() === proto.LogEventType.DEPRECATION_WARNING, + deprecation: event.type === proto.LogEventType.DEPRECATION_WARNING, }; - const spanProto = event.getSpan(); + const spanProto = event.span; if (spanProto) params.span = deprotofySourceSpan(spanProto); - const stack = event.getStackTrace(); + const stack = event.stackTrace; if (stack) params.stack = stack; - options.logger.warn(event.getMessage(), params); + options.logger.warn(event.message, params); } else { - console.error(event.getFormatted()); + console.error(event.formatted); } } } @@ -303,20 +309,20 @@ function handleLogEvent( * Throws a `SassException` if the compilation failed. */ function handleCompileResponse( - response: proto.OutboundMessage.CompileResponse + response: proto.OutboundMessage_CompileResponse ): CompileResult { - if (response.getSuccess()) { - const success = response.getSuccess()!; + if (response.result.case === 'success') { + const success = response.result.value; const result: CompileResult = { - css: success.getCss(), - loadedUrls: success.getLoadedUrlsList().map(url => new URL(url)), + css: success.css, + loadedUrls: response.loadedUrls.map(url => new URL(url)), }; - const sourceMap = success.getSourceMap(); + const sourceMap = success.sourceMap; if (sourceMap) result.sourceMap = JSON.parse(sourceMap); return result; - } else if (response.getFailure()) { - throw new Exception(response.getFailure()!); + } else if (response.result.case === 'failure') { + throw new Exception(response.result.value); } else { throw utils.compilerError('Compiler sent empty CompileResponse.'); } diff --git a/lib/src/compiler-path.ts b/lib/src/compiler-path.ts index 6f8c7a1b..0e193c95 100644 --- a/lib/src/compiler-path.ts +++ b/lib/src/compiler-path.ts @@ -6,27 +6,43 @@ import * as fs from 'fs'; import * as p from 'path'; import {isErrnoException} from './utils'; -/** The path to the embedded compiler executable. */ -export const compilerPath = (() => { +/** The full command for the embedded compiler executable. */ +export const compilerCommand = (() => { // find for development for (const path of ['vendor', '../../../lib/src/vendor']) { const executable = p.resolve( __dirname, path, - `dart-sass-embedded/dart-sass-embedded${ - process.platform === 'win32' ? '.bat' : '' - }` + `dart-sass/sass${process.platform === 'win32' ? '.bat' : ''}` ); - if (fs.existsSync(executable)) return executable; + if (fs.existsSync(executable)) return [executable]; } try { - return require.resolve( - `sass-embedded-${process.platform}-${process.arch}/` + - 'dart-sass-embedded/dart-sass-embedded' + - (process.platform === 'win32' ? '.bat' : '') - ); + return [ + require.resolve( + `sass-embedded-${process.platform}-${process.arch}/` + + 'dart-sass/src/dart' + + (process.platform === 'win32' ? '.exe' : '') + ), + require.resolve( + `sass-embedded-${process.platform}-${process.arch}/` + + 'dart-sass/src/sass.snapshot' + ), + ]; + } catch (ignored) { + // ignored + } + + try { + return [ + require.resolve( + `sass-embedded-${process.platform}-${process.arch}/` + + 'dart-sass/sass' + + (process.platform === 'win32' ? '.bat' : '') + ), + ]; } catch (e: unknown) { if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) { throw e; diff --git a/lib/src/deprotofy-span.ts b/lib/src/deprotofy-span.ts index 950ecce4..bd32762f 100644 --- a/lib/src/deprotofy-span.ts +++ b/lib/src/deprotofy-span.ts @@ -4,54 +4,42 @@ import {URL} from 'url'; -import * as proto from './vendor/embedded-protocol/embedded_sass_pb'; -import {SourceLocation, SourceSpan} from './vendor/sass'; +import * as proto from './vendor/embedded_sass_pb'; +import {SourceSpan} from './vendor/sass'; import {compilerError} from './utils'; // Creates a SourceSpan from the given protocol `buffer`. Throws if the buffer // has invalid fields. export function deprotofySourceSpan(buffer: proto.SourceSpan): SourceSpan { - const text = buffer.getText(); + const text = buffer.text; - if (buffer.getStart() === undefined) { + if (buffer.start === undefined) { throw compilerError('Expected SourceSpan to have start.'); } - const start = deprotofySourceLocation(buffer.getStart()!); let end; - if (buffer.getEnd() === undefined) { + if (buffer.end === undefined) { if (text !== '') { throw compilerError('Expected SourceSpan text to be empty.'); } else { - end = start; + end = buffer.start; } } else { - end = deprotofySourceLocation(buffer.getEnd()!); - if (end.offset < start.offset) { + end = buffer.end; + if (end.offset < buffer.start.offset) { throw compilerError('Expected SourceSpan end to be after start.'); } } - const url = buffer.getUrl() === '' ? undefined : new URL(buffer.getUrl()); + const url = buffer.url === '' ? undefined : new URL(buffer.url); - const context = buffer.getContext() === '' ? undefined : buffer.getContext(); + const context = buffer.context === '' ? undefined : buffer.context; return { text, - start, + start: buffer.start, end, url, context, }; } - -// Creates a SourceLocation from the given protocol `buffer`. -function deprotofySourceLocation( - buffer: proto.SourceSpan.SourceLocation -): SourceLocation { - return { - offset: buffer.getOffset(), - line: buffer.getLine(), - column: buffer.getColumn(), - }; -} diff --git a/lib/src/dispatcher.ts b/lib/src/dispatcher.ts index 2dc77426..08d1271a 100644 --- a/lib/src/dispatcher.ts +++ b/lib/src/dispatcher.ts @@ -5,25 +5,13 @@ import {Observable, Subject} from 'rxjs'; import {filter, map, mergeMap} from 'rxjs/operators'; -import { - InboundMessage, - OutboundMessage, -} from './vendor/embedded-protocol/embedded_sass_pb'; -import { - InboundRequest, - InboundRequestType, - InboundResponse, - InboundResponseType, - InboundTypedMessage, - OutboundResponse, - OutboundResponseType, - OutboundTypedMessage, -} from './message-transformer'; +import {OutboundResponse} from './messages'; +import * as proto from './vendor/embedded_sass_pb'; import {RequestTracker} from './request-tracker'; -import {PromiseOr, thenOr} from './utils'; +import {PromiseOr, compilerError, thenOr, hostError} from './utils'; /** - * Dispatches requests, responses, and events. + * Dispatches requests, responses, and events for a single compilation. * * Accepts callbacks for processing different types of outbound requests. When * an outbound request arrives, this runs the appropriate callback to process @@ -43,17 +31,13 @@ import {PromiseOr, thenOr} from './utils'; * perform proper error handling. */ export class Dispatcher { - // Tracks the IDs of all inbound requests. An outbound response with matching - // ID and type will remove the ID. - private readonly pendingInboundRequests = new RequestTracker(); - // Tracks the IDs of all outbound requests. An inbound response with matching // ID and type will remove the ID. private readonly pendingOutboundRequests = new RequestTracker(); - // All outbound messages. If we detect any errors while dispatching messages, - // this completes. - private readonly messages$ = new Subject(); + // All outbound messages for this compilation. If we detect any errors while + // dispatching messages, this completes. + private readonly messages$ = new Subject(); // If the dispatcher encounters an error, this errors out. It is publicly // exposed as a readonly Observable. @@ -71,19 +55,28 @@ export class Dispatcher { * silently. */ readonly logEvents$ = this.messages$.pipe( - filter(message => message.type === OutboundMessage.MessageCase.LOG_EVENT), - map(message => message.payload as OutboundMessage.LogEvent) + filter(message => message.message.case === 'logEvent'), + map(message => message.message.value as proto.OutboundMessage_LogEvent) ); constructor( - private readonly outboundMessages$: Observable, + private readonly compilationId: number, + private readonly outboundMessages$: Observable< + [number, proto.OutboundMessage] + >, private readonly writeInboundMessage: ( - message: InboundTypedMessage + message: [number, proto.InboundMessage] ) => void, private readonly outboundRequestHandlers: DispatcherHandlers ) { + if (compilationId < 1) { + throw Error(`Invalid compilation ID ${compilationId}.`); + } + this.outboundMessages$ .pipe( + filter(([compilationId]) => compilationId === this.compilationId), + map(([, message]) => message), mergeMap(message => { const result = this.handleOutboundMessage(message); return result instanceof Promise @@ -110,18 +103,36 @@ export class Dispatcher { * events synchronously, `callback` will be called synchronously. */ sendCompileRequest( - request: InboundMessage.CompileRequest, + request: proto.InboundMessage_CompileRequest, callback: ( err: unknown, - response: OutboundMessage.CompileResponse | undefined + response: proto.OutboundMessage_CompileResponse | undefined ) => void ): void { - this.handleInboundRequest( - request, - InboundMessage.MessageCase.COMPILE_REQUEST, - OutboundMessage.MessageCase.COMPILE_RESPONSE, - callback - ); + if (this.messages$.isStopped) { + callback(new Error('Tried writing to closed dispatcher'), undefined); + return; + } + + this.messages$ + .pipe( + filter(message => message.message.case === 'compileResponse'), + map(message => message.message.value as OutboundResponse) + ) + .subscribe({next: response => callback(null, response)}); + + this.error$.subscribe({error: error => callback(error, undefined)}); + + try { + this.writeInboundMessage([ + this.compilationId, + new proto.InboundMessage({ + message: {value: request, case: 'compileRequest'}, + }), + ]); + } catch (error) { + this.throwAndClose(error); + } } // Rejects with `error` all promises awaiting an outbound response, and @@ -136,140 +147,103 @@ export class Dispatcher { // contains a request, runs the appropriate callback to generate an inbound // response, and then sends it inbound. private handleOutboundMessage( - message: OutboundTypedMessage + message: proto.OutboundMessage ): PromiseOr { - switch (message.type) { - case OutboundMessage.MessageCase.LOG_EVENT: + switch (message.message.case) { + case 'logEvent': + // Handled separately by `logEvents$`. return undefined; - case OutboundMessage.MessageCase.COMPILE_RESPONSE: - this.pendingInboundRequests.resolve( - (message.payload as OutboundResponse).getId(), - message.type - ); + case 'compileResponse': + // Handled separately by `sendCompileRequest`. return undefined; - case OutboundMessage.MessageCase.IMPORT_REQUEST: { - const request = message.payload as OutboundMessage.ImportRequest; - const id = request.getId(); - const type = InboundMessage.MessageCase.IMPORT_RESPONSE; + case 'importRequest': { + const request = message.message.value; + const id = request.id; + const type = 'importResponse'; this.pendingOutboundRequests.add(id, type); return thenOr( this.outboundRequestHandlers.handleImportRequest(request), response => { - this.sendInboundMessage(id, response, type); + this.sendInboundMessage(id, {case: type, value: response}); } ); } - case OutboundMessage.MessageCase.FILE_IMPORT_REQUEST: { - const request = message.payload as OutboundMessage.FileImportRequest; - const id = request.getId(); - const type = InboundMessage.MessageCase.FILE_IMPORT_RESPONSE; + case 'fileImportRequest': { + const request = message.message.value; + const id = request.id; + const type = 'fileImportResponse'; this.pendingOutboundRequests.add(id, type); return thenOr( this.outboundRequestHandlers.handleFileImportRequest(request), response => { - this.sendInboundMessage(id, response, type); + this.sendInboundMessage(id, {case: type, value: response}); } ); } - case OutboundMessage.MessageCase.CANONICALIZE_REQUEST: { - const request = message.payload as OutboundMessage.CanonicalizeRequest; - const id = request.getId(); - const type = InboundMessage.MessageCase.CANONICALIZE_RESPONSE; + case 'canonicalizeRequest': { + const request = message.message.value; + const id = request.id; + const type = 'canonicalizeResponse'; this.pendingOutboundRequests.add(id, type); return thenOr( this.outboundRequestHandlers.handleCanonicalizeRequest(request), response => { - this.sendInboundMessage(id, response, type); + this.sendInboundMessage(id, {case: type, value: response}); } ); } - case OutboundMessage.MessageCase.FUNCTION_CALL_REQUEST: { - const request = message.payload as OutboundMessage.FunctionCallRequest; - const id = request.getId(); - const type = InboundMessage.MessageCase.FUNCTION_CALL_RESPONSE; + case 'functionCallRequest': { + const request = message.message.value; + const id = request.id; + const type = 'functionCallResponse'; this.pendingOutboundRequests.add(id, type); return thenOr( this.outboundRequestHandlers.handleFunctionCallRequest(request), response => { - this.sendInboundMessage(id, response, type); + this.sendInboundMessage(id, {case: type, value: response}); } ); } - default: - throw Error(`Unknown message type ${message.type}`); - } - } - - // Sends a `request` of type `requestType` inbound. Returns a promise that - // will either resolve with the corresponding outbound response of type - // `responseType`, or error if any Protocol Errors were encountered. - private handleInboundRequest( - request: InboundRequest, - requestType: InboundRequestType, - responseType: OutboundResponseType, - callback: (err: unknown, response: OutboundResponse | undefined) => void - ): void { - if (this.messages$.isStopped) { - callback(new Error('Tried writing to closed dispatcher'), undefined); - return; - } - - this.messages$ - .pipe( - filter(message => message.type === responseType), - map(message => message.payload as OutboundResponse), - filter(response => response.getId() === request.getId()) - ) - .subscribe({next: response => callback(null, response)}); - - this.error$.subscribe({error: error => callback(error, undefined)}); + case 'error': + throw hostError(message.message.value.message); - try { - this.sendInboundMessage( - this.pendingInboundRequests.nextId, - request, - requestType - ); - } catch (error) { - this.throwAndClose(error); + default: + throw compilerError(`Unknown message type ${message.message.case}`); } } // Sends a message inbound. Keeps track of all pending inbound requests. private sendInboundMessage( - id: number, - payload: InboundRequest | InboundResponse, - type: InboundRequestType | InboundResponseType + requestId: number, + message: Exclude< + proto.InboundMessage['message'], + {case: undefined | 'compileRequest'} + > ): void { - payload.setId(id); + message.value.id = requestId; - if (type === InboundMessage.MessageCase.COMPILE_REQUEST) { - this.pendingInboundRequests.add( - id, - OutboundMessage.MessageCase.COMPILE_RESPONSE - ); - } else if ( - type === InboundMessage.MessageCase.IMPORT_RESPONSE || - type === InboundMessage.MessageCase.FILE_IMPORT_RESPONSE || - type === InboundMessage.MessageCase.CANONICALIZE_RESPONSE || - type === InboundMessage.MessageCase.FUNCTION_CALL_RESPONSE + if ( + message.case === 'importResponse' || + message.case === 'fileImportResponse' || + message.case === 'canonicalizeResponse' || + message.case === 'functionCallResponse' ) { - this.pendingOutboundRequests.resolve(id, type); + this.pendingOutboundRequests.resolve(requestId, message.case); } else { - throw Error(`Unknown message type ${type}`); + throw Error(`Unknown message type ${message.case}`); } - this.writeInboundMessage({ - payload, - type, - }); + this.writeInboundMessage([ + this.compilationId, + new proto.InboundMessage({message}), + ]); } } @@ -278,15 +252,15 @@ export class Dispatcher { */ export interface DispatcherHandlers { handleImportRequest: ( - request: OutboundMessage.ImportRequest - ) => PromiseOr; + request: proto.OutboundMessage_ImportRequest + ) => PromiseOr; handleFileImportRequest: ( - request: OutboundMessage.FileImportRequest - ) => PromiseOr; + request: proto.OutboundMessage_FileImportRequest + ) => PromiseOr; handleCanonicalizeRequest: ( - request: OutboundMessage.CanonicalizeRequest - ) => PromiseOr; + request: proto.OutboundMessage_CanonicalizeRequest + ) => PromiseOr; handleFunctionCallRequest: ( - request: OutboundMessage.FunctionCallRequest - ) => PromiseOr; + request: proto.OutboundMessage_FunctionCallRequest + ) => PromiseOr; } diff --git a/lib/src/exception.ts b/lib/src/exception.ts index ca2a2a58..48eb6dfd 100644 --- a/lib/src/exception.ts +++ b/lib/src/exception.ts @@ -2,7 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import * as proto from './vendor/embedded-protocol/embedded_sass_pb'; +import * as proto from './vendor/embedded_sass_pb'; import {Exception as SassException, SourceSpan} from './vendor/sass'; import {deprotofySourceSpan} from './deprotofy-span'; @@ -11,12 +11,12 @@ export class Exception extends Error implements SassException { readonly sassStack: string; readonly span: SourceSpan; - constructor(failure: proto.OutboundMessage.CompileResponse.CompileFailure) { - super(failure.getFormatted()); + constructor(failure: proto.OutboundMessage_CompileResponse_CompileFailure) { + super(failure.formatted); - this.sassMessage = failure.getMessage(); - this.sassStack = failure.getStackTrace(); - this.span = deprotofySourceSpan(failure.getSpan()!); + this.sassMessage = failure.message; + this.sassStack = failure.stackTrace; + this.span = deprotofySourceSpan(failure.span!); } toString() { diff --git a/lib/src/function-registry.ts b/lib/src/function-registry.ts index dc0c0e62..d82acc4a 100644 --- a/lib/src/function-registry.ts +++ b/lib/src/function-registry.ts @@ -7,11 +7,8 @@ import {inspect} from 'util'; import * as types from './vendor/sass'; import * as utils from './utils'; import {CustomFunction} from './vendor/sass'; -import { - InboundMessage, - OutboundMessage, -} from './vendor/embedded-protocol/embedded_sass_pb'; -import {PromiseOr, catchOr, thenOr} from './utils'; +import * as proto from './vendor/embedded_sass_pb'; +import {PromiseOr, catchOr, compilerError, thenOr} from './utils'; import {Protofier} from './protofier'; import {Value} from './value'; @@ -55,8 +52,8 @@ export class FunctionRegistry { * Returns the function to which `request` refers and returns its response. */ call( - request: OutboundMessage.FunctionCallRequest - ): PromiseOr { + request: proto.OutboundMessage_FunctionCallRequest + ): PromiseOr { const protofier = new Protofier(this); const fn = this.get(request); @@ -64,61 +61,60 @@ export class FunctionRegistry { () => { return thenOr( fn( - request - .getArgumentsList() - .map(value => protofier.deprotofy(value) as types.Value) + request.arguments.map( + value => protofier.deprotofy(value) as types.Value + ) ), result => { if (!(result instanceof Value)) { const name = - request.getName().length === 0 - ? 'anonymous function' - : `"${request.getName()}"`; + request.identifier.case === 'name' + ? `"${request.identifier.value}"` + : 'anonymous function'; throw ( `options.functions: ${name} returned non-Value: ` + inspect(result) ); } - const response = new InboundMessage.FunctionCallResponse(); - response.setSuccess(protofier.protofy(result)); - response.setAccessedArgumentListsList( - protofier.accessedArgumentLists - ); - return response; + return new proto.InboundMessage_FunctionCallResponse({ + result: {case: 'success', value: protofier.protofy(result)}, + accessedArgumentLists: protofier.accessedArgumentLists, + }); } ); }, - error => { - const response = new InboundMessage.FunctionCallResponse(); - response.setError(`${error}`); - return response; - } + error => + new proto.InboundMessage_FunctionCallResponse({ + result: {case: 'error', value: `${error}`}, + }) ); } /** Returns the function to which `request` refers. */ private get( - request: OutboundMessage.FunctionCallRequest + request: proto.OutboundMessage_FunctionCallRequest ): CustomFunction { - if ( - request.getIdentifierCase() === - OutboundMessage.FunctionCallRequest.IdentifierCase.NAME - ) { - const fn = this.functionsByName.get(request.getName()); + if (request.identifier.case === 'name') { + const fn = this.functionsByName.get(request.identifier.value); if (fn) return fn; - throw new Error( - 'Invalid OutboundMessage.FunctionCallRequest: there is no function ' + - `named "${request.getName()}"` + throw compilerError( + 'Invalid OutboundMessage_FunctionCallRequest: there is no function ' + + `named "${request.identifier.value}"` ); - } else { - const fn = this.functionsById.get(request.getFunctionId()); + } else if (request.identifier.case === 'functionId') { + const fn = this.functionsById.get(request.identifier.value); if (fn) return fn; - throw new Error( - 'Invalid OutboundMessage.FunctionCallRequest: there is no function ' + - `with ID "${request.getFunctionId()}"` + throw compilerError( + 'Invalid OutboundMessage_FunctionCallRequest: there is no function ' + + `with ID "${request.identifier.value}"` + ); + } else { + throw compilerError( + 'Invalid OutboundMessage_FunctionCallRequest: function identifier is ' + + 'unset' ); } } diff --git a/lib/src/importer-registry.ts b/lib/src/importer-registry.ts index 94e3a046..7b734940 100644 --- a/lib/src/importer-registry.ts +++ b/lib/src/importer-registry.ts @@ -8,10 +8,7 @@ import {inspect} from 'util'; import * as utils from './utils'; import {FileImporter, Importer, Options} from './vendor/sass'; -import { - InboundMessage, - OutboundMessage, -} from './vendor/embedded-protocol/embedded_sass_pb'; +import * as proto from './vendor/embedded_sass_pb'; import {catchOr, thenOr, PromiseOr} from './utils'; /** @@ -20,7 +17,7 @@ import {catchOr, thenOr, PromiseOr} from './utils'; */ export class ImporterRegistry { /** Protocol buffer representations of the registered importers. */ - readonly importers: InboundMessage.CompileRequest.Importer[]; + readonly importers: proto.InboundMessage_CompileRequest_Importer[]; /** A map from importer IDs to their corresponding importers. */ private readonly importersById = new Map>(); @@ -35,19 +32,20 @@ export class ImporterRegistry { this.importers = (options?.importers ?? []) .map(importer => this.register(importer)) .concat( - (options?.loadPaths ?? []).map(path => { - const proto = new InboundMessage.CompileRequest.Importer(); - proto.setPath(p.resolve(path)); - return proto; - }) + (options?.loadPaths ?? []).map( + path => + new proto.InboundMessage_CompileRequest_Importer({ + importer: {case: 'path', value: p.resolve(path)}, + }) + ) ); } /** Converts an importer to a proto without adding it to `this.importers`. */ register( importer: Importer | FileImporter - ): InboundMessage.CompileRequest.Importer { - const proto = new InboundMessage.CompileRequest.Importer(); + ): proto.InboundMessage_CompileRequest_Importer { + const response = new proto.InboundMessage_CompileRequest_Importer(); if ('canonicalize' in importer) { if ('findFileUrl' in importer) { throw new Error( @@ -56,21 +54,21 @@ export class ImporterRegistry { ); } - proto.setImporterId(this.id); + response.importer = {case: 'importerId', value: this.id}; this.importersById.set(this.id, importer); } else { - proto.setFileImporterId(this.id); + response.importer = {case: 'fileImporterId', value: this.id}; this.fileImportersById.set(this.id, importer); } this.id += 1; - return proto; + return response; } /** Handles a canonicalization request. */ canonicalize( - request: OutboundMessage.CanonicalizeRequest - ): PromiseOr { - const importer = this.importersById.get(request.getImporterId()); + request: proto.OutboundMessage_CanonicalizeRequest + ): PromiseOr { + const importer = this.importersById.get(request.importerId); if (!importer) { throw utils.compilerError('Unknown CanonicalizeRequest.importer_id'); } @@ -78,77 +76,78 @@ export class ImporterRegistry { return catchOr( () => { return thenOr( - importer.canonicalize(request.getUrl(), { - fromImport: request.getFromImport(), + importer.canonicalize(request.url, { + fromImport: request.fromImport, }), - url => { - const proto = new InboundMessage.CanonicalizeResponse(); - if (url !== null) proto.setUrl(url.toString()); - return proto; - } + url => + new proto.InboundMessage_CanonicalizeResponse({ + result: + url === null + ? {case: undefined} + : {case: 'url', value: url.toString()}, + }) ); }, - error => { - const proto = new InboundMessage.CanonicalizeResponse(); - proto.setError(`${error}`); - return proto; - } + error => + new proto.InboundMessage_CanonicalizeResponse({ + result: {case: 'error', value: `${error}`}, + }) ); } /** Handles an import request. */ import( - request: OutboundMessage.ImportRequest - ): PromiseOr { - const importer = this.importersById.get(request.getImporterId()); + request: proto.OutboundMessage_ImportRequest + ): PromiseOr { + const importer = this.importersById.get(request.importerId); if (!importer) { throw utils.compilerError('Unknown ImportRequest.importer_id'); } return catchOr( () => { - return thenOr(importer.load(new URL(request.getUrl())), result => { - const proto = new InboundMessage.ImportResponse(); - if (result) { - if (typeof result.contents !== 'string') { - throw Error( - `Invalid argument (contents): must be a string but was: ${ - (result.contents as {}).constructor.name - }` - ); - } - - if (result.sourceMapUrl && !result.sourceMapUrl.protocol) { - throw Error( - 'Invalid argument (sourceMapUrl): must be absolute but was: ' + - result.sourceMapUrl - ); - } + return thenOr(importer.load(new URL(request.url)), result => { + if (!result) return new proto.InboundMessage_ImportResponse(); + + if (typeof result.contents !== 'string') { + throw Error( + `Invalid argument (contents): must be a string but was: ${ + (result.contents as {}).constructor.name + }` + ); + } - const success = new InboundMessage.ImportResponse.ImportSuccess(); - success.setContents(result.contents); - success.setSyntax(utils.protofySyntax(result.syntax)); - if (result.sourceMapUrl) { - success.setSourceMapUrl(result.sourceMapUrl.toString()); - } - proto.setSuccess(success); + if (result.sourceMapUrl && !result.sourceMapUrl.protocol) { + throw Error( + 'Invalid argument (sourceMapUrl): must be absolute but was: ' + + result.sourceMapUrl + ); } - return proto; + + return new proto.InboundMessage_ImportResponse({ + result: { + case: 'success', + value: new proto.InboundMessage_ImportResponse_ImportSuccess({ + contents: result.contents, + syntax: utils.protofySyntax(result.syntax), + sourceMapUrl: result.sourceMapUrl?.toString() ?? '', + }), + }, + }); }); }, - error => { - const proto = new InboundMessage.ImportResponse(); - proto.setError(`${error}`); - return proto; - } + error => + new proto.InboundMessage_ImportResponse({ + result: {case: 'error', value: `${error}`}, + }) ); } /** Handles a file import request. */ fileImport( - request: OutboundMessage.FileImportRequest - ): PromiseOr { - const importer = this.fileImportersById.get(request.getImporterId()); + request: proto.OutboundMessage_FileImportRequest + ): PromiseOr { + const importer = this.fileImportersById.get(request.importerId); if (!importer) { throw utils.compilerError('Unknown FileImportRequest.importer_id'); } @@ -156,29 +155,27 @@ export class ImporterRegistry { return catchOr( () => { return thenOr( - importer.findFileUrl(request.getUrl(), { - fromImport: request.getFromImport(), + importer.findFileUrl(request.url, { + fromImport: request.fromImport, }), url => { - const proto = new InboundMessage.FileImportResponse(); - if (url) { - if (url.protocol !== 'file:') { - throw ( - `FileImporter ${inspect(importer)} returned non-file: URL ` + - +`"${url}" for URL "${request.getUrl()}".` - ); - } - proto.setFileUrl(url.toString()); + if (!url) return new proto.InboundMessage_FileImportResponse(); + if (url.protocol !== 'file:') { + throw ( + `FileImporter ${inspect(importer)} returned non-file: URL ` + + +`"${url}" for URL "${request.url}".` + ); } - return proto; + return new proto.InboundMessage_FileImportResponse({ + result: {case: 'fileUrl', value: url.toString()}, + }); } ); }, - error => { - const proto = new InboundMessage.FileImportResponse(); - proto.setError(`${error}`); - return proto; - } + error => + new proto.InboundMessage_FileImportResponse({ + result: {case: 'error', value: `${error}`}, + }) ); } } diff --git a/lib/src/legacy/importer.ts b/lib/src/legacy/importer.ts index 0921c329..149c55cf 100644 --- a/lib/src/legacy/importer.ts +++ b/lib/src/legacy/importer.ts @@ -125,7 +125,9 @@ export class LegacyImporterWrapper this.expectingRelativeLoad = false; return null; - } else { + } else if (!url.startsWith('file:')) { + // We'll only search for another relative import when the URL isn't + // absolute. this.expectingRelativeLoad = true; } diff --git a/lib/src/legacy/index.ts b/lib/src/legacy/index.ts index 41fafec4..1631a534 100644 --- a/lib/src/legacy/index.ts +++ b/lib/src/legacy/index.ts @@ -83,6 +83,10 @@ function adjustOptions( throw new Error('Either options.data or options.file must be set.'); } + // In legacy API, the current working directory is always attempted before + // any load path. + options.includePaths = [process.cwd(), ...(options.includePaths ?? [])]; + if ( !isStringOptions(options) && // The `indentedSyntax` option takes precedence over the file extension in the @@ -200,9 +204,7 @@ function pluginThis( context: undefined as unknown as LegacyPluginThis, file: options.file, data: options.data, - includePaths: [process.cwd(), ...(options.includePaths ?? [])].join( - p.delimiter - ), + includePaths: (options.includePaths ?? []).join(p.delimiter), precision: 10, style: 1, indentType: 0, diff --git a/lib/src/message-transformer.test.ts b/lib/src/message-transformer.test.ts index 641ac325..d74f2e28 100644 --- a/lib/src/message-transformer.test.ts +++ b/lib/src/message-transformer.test.ts @@ -3,30 +3,33 @@ // https://opensource.org/licenses/MIT. import {Subject, Observable} from 'rxjs'; +import * as varint from 'varint'; import {expectObservableToError} from '../../test/utils'; -import {MessageTransformer, OutboundTypedMessage} from './message-transformer'; -import { - InboundMessage, - OutboundMessage, - ProtocolError, -} from './vendor/embedded-protocol/embedded_sass_pb'; +import {MessageTransformer} from './message-transformer'; +import * as proto from './vendor/embedded_sass_pb'; describe('message transformer', () => { let messages: MessageTransformer; - function validInboundMessage(source: string): InboundMessage { - const input = new InboundMessage.CompileRequest.StringInput(); - input.setSource(source); - const request = new InboundMessage.CompileRequest(); - request.setString(input); - const message = new InboundMessage(); - message.setCompileRequest(request); - return message; + function validInboundMessage(source: string): proto.InboundMessage { + return new proto.InboundMessage({ + message: { + case: 'compileRequest', + value: new proto.InboundMessage_CompileRequest({ + input: { + case: 'string', + value: new proto.InboundMessage_CompileRequest_StringInput({ + source, + }), + }, + }), + }, + }); } describe('encode', () => { - let encodedProtobufs: Buffer[]; + let encodedProtobufs: Uint8Array[]; beforeEach(() => { encodedProtobufs = []; @@ -37,21 +40,16 @@ describe('message transformer', () => { it('encodes an InboundMessage to buffer', () => { const message = validInboundMessage('a {b: c}'); - - messages.writeInboundMessage({ - payload: message.getCompileRequest()!, - type: InboundMessage.MessageCase.COMPILE_REQUEST, - }); - + messages.writeInboundMessage([1234, message]); expect(encodedProtobufs).toEqual([ - Buffer.from(message.serializeBinary()), + Uint8Array.from([...varint.encode(1234), ...message.toBinary()]), ]); }); }); describe('decode', () => { - let protobufs$: Subject; - let decodedMessages: OutboundTypedMessage[]; + let protobufs$: Subject; + let decodedMessages: Array<[number, proto.OutboundMessage]>; beforeEach(() => { protobufs$ = new Subject(); @@ -60,22 +58,32 @@ describe('message transformer', () => { }); it('decodes buffer to OutboundMessage', done => { - const message = validInboundMessage('a {b: c}'); - messages.outboundMessages$.subscribe({ next: message => decodedMessages.push(message), complete: () => { expect(decodedMessages.length).toBe(1); - const response = decodedMessages[0] - .payload as OutboundMessage.CompileResponse; - expect(response.getSuccess()?.getCss()).toBe('a {b: c}'); - const type = decodedMessages[0].type; - expect(type).toEqual(OutboundMessage.MessageCase.COMPILE_RESPONSE); + const [id, message] = decodedMessages[0]; + expect(id).toBe(1234); + expect(message.message.case).toBe('compileResponse'); + const response = message.message + .value as proto.OutboundMessage_CompileResponse; + expect(response.result.case).toBe('success'); + expect( + ( + response.result + .value as proto.OutboundMessage_CompileResponse_CompileSuccess + ).css + ).toBe('a {b: c}'); done(); }, }); - protobufs$.next(Buffer.from(message.serializeBinary())); + protobufs$.next( + Uint8Array.from([ + ...varint.encode(1234), + ...validInboundMessage('a {b: c}').toBinary(), + ]) + ); protobufs$.complete(); }); @@ -83,63 +91,13 @@ describe('message transformer', () => { it('fails on invalid buffer', done => { expectObservableToError( messages.outboundMessages$, - 'Compiler caused error: Invalid buffer.', + 'Compiler caused error: Invalid compilation ID varint: RangeError: ' + + 'Could not decode varint.', done ); protobufs$.next(Buffer.from([-1])); }); - - it('fails on empty message', done => { - expectObservableToError( - messages.outboundMessages$, - 'Compiler caused error: OutboundMessage.message is not set.', - done - ); - - protobufs$.next(Buffer.from(new OutboundMessage().serializeBinary())); - }); - - it('fails on compile response with missing result', done => { - expectObservableToError( - messages.outboundMessages$, - 'Compiler caused error: OutboundMessage.CompileResponse.result is not set.', - done - ); - - const response = new OutboundMessage.CompileResponse(); - const message = new OutboundMessage(); - message.setCompileResponse(response); - protobufs$.next(Buffer.from(message.serializeBinary())); - }); - - it('fails on function call request with missing identifier', done => { - expectObservableToError( - messages.outboundMessages$, - 'Compiler caused error: OutboundMessage.FunctionCallRequest.identifier is not set.', - done - ); - - const request = new OutboundMessage.FunctionCallRequest(); - const message = new OutboundMessage(); - message.setFunctionCallRequest(request); - protobufs$.next(Buffer.from(message.serializeBinary())); - }); - - it('fails if message contains a protocol error', done => { - const errorMessage = 'sad'; - expectObservableToError( - messages.outboundMessages$, - `Compiler reported error: ${errorMessage}.`, - done - ); - - const error = new ProtocolError(); - error.setMessage(errorMessage); - const message = new OutboundMessage(); - message.setError(error); - protobufs$.next(Buffer.from(message.serializeBinary())); - }); }); }); }); diff --git a/lib/src/message-transformer.ts b/lib/src/message-transformer.ts index bce04283..703b0c96 100644 --- a/lib/src/message-transformer.ts +++ b/lib/src/message-transformer.ts @@ -4,85 +4,31 @@ import {Observable, Subject} from 'rxjs'; import {map} from 'rxjs/operators'; +import * as varint from 'varint'; -import {compilerError, hostError} from './utils'; -import { - InboundMessage, - OutboundMessage, -} from './vendor/embedded-protocol/embedded_sass_pb'; - -export type InboundRequestType = InboundMessage.MessageCase.COMPILE_REQUEST; - -export type InboundRequest = InboundMessage.CompileRequest; - -export type InboundResponseType = - | InboundMessage.MessageCase.IMPORT_RESPONSE - | InboundMessage.MessageCase.FILE_IMPORT_RESPONSE - | InboundMessage.MessageCase.CANONICALIZE_RESPONSE - | InboundMessage.MessageCase.FUNCTION_CALL_RESPONSE; - -export type InboundResponse = - | InboundMessage.ImportResponse - | InboundMessage.FileImportResponse - | InboundMessage.CanonicalizeResponse - | InboundMessage.FunctionCallResponse; - -export type OutboundRequestType = - | OutboundMessage.MessageCase.IMPORT_REQUEST - | OutboundMessage.MessageCase.FILE_IMPORT_REQUEST - | OutboundMessage.MessageCase.CANONICALIZE_REQUEST - | OutboundMessage.MessageCase.FUNCTION_CALL_REQUEST; - -export type OutboundRequest = - | OutboundMessage.ImportRequest - | OutboundMessage.FileImportRequest - | OutboundMessage.CanonicalizeRequest - | OutboundMessage.FunctionCallRequest; - -export type OutboundResponseType = OutboundMessage.MessageCase.COMPILE_RESPONSE; - -export type OutboundResponse = OutboundMessage.CompileResponse; - -export type OutboundEventType = OutboundMessage.MessageCase.LOG_EVENT; - -export type OutboundEvent = OutboundMessage.LogEvent; - -export type InboundTypedMessage = { - payload: InboundRequest | InboundResponse; - type: InboundRequestType | InboundResponseType; -}; - -export type OutboundTypedMessage = { - payload: OutboundRequest | OutboundResponse | OutboundEvent; - type: OutboundRequestType | OutboundResponseType | OutboundEventType; -}; +import {compilerError} from './utils'; +import {InboundMessage, OutboundMessage} from './vendor/embedded_sass_pb'; /** - * Encodes InboundTypedMessages into protocol buffers and decodes protocol - * buffers into OutboundTypedMessages. Any Embedded Protocol violations that can - * be detected at the message level are encapsulated here and reported as - * errors. - * - * This transformer communicates via In/OutboundTypedMessages instead of raw - * In/OutboundMessages in order to expose more type information to consumers. - * This makes the stream of messages from the transformer easier to interact - * with. + * Encodes InboundMessages into protocol buffers and decodes protocol buffers + * into OutboundMessages. */ export class MessageTransformer { // The decoded messages are written to this Subject. It is publicly exposed // as a readonly Observable. - private readonly outboundMessagesInternal$ = - new Subject(); + private readonly outboundMessagesInternal$ = new Subject< + [number, OutboundMessage] + >(); /** - * The OutboundTypedMessages, decoded from protocol buffers. If any errors are - * detected while encoding/decoding, this Observable will error out. + * The OutboundMessages, decoded from protocol buffers. If this fails to + * decode a message, it will emit an error. */ readonly outboundMessages$ = this.outboundMessagesInternal$.pipe(); constructor( - private readonly outboundProtobufs$: Observable, - private readonly writeInboundProtobuf: (buffer: Buffer) => void + private readonly outboundProtobufs$: Observable, + private readonly writeInboundProtobuf: (buffer: Uint8Array) => void ) { this.outboundProtobufs$ .pipe(map(decode)) @@ -90,109 +36,46 @@ export class MessageTransformer { } /** - * Converts the inbound `message` to a protocol buffer. + * Converts the inbound `compilationId` and `message` to a protocol buffer. */ - writeInboundMessage(message: InboundTypedMessage): void { + writeInboundMessage([compilationId, message]: [ + number, + InboundMessage + ]): void { + const compilationIdLength = varint.encodingLength(compilationId); + const encodedMessage = message.toBinary(); + const buffer = new Uint8Array(compilationIdLength + encodedMessage.length); + varint.encode(compilationId, buffer); + buffer.set(encodedMessage, compilationIdLength); + try { - this.writeInboundProtobuf(encode(message)); + this.writeInboundProtobuf(buffer); } catch (error) { this.outboundMessagesInternal$.error(error); } } } -// Decodes a protobuf `buffer` into an OutboundTypedMessage, ensuring that all -// mandatory message fields are populated. Throws if `buffer` cannot be decoded -// into a valid message, or if the message itself contains a Protocol Error. -function decode(buffer: Buffer): OutboundTypedMessage { - let message; +// Decodes a protobuf `buffer` into a compilation ID and an OutboundMessage, +// ensuring that all mandatory message fields are populated. Throws if `buffer` +// cannot be decoded into a valid message, or if the message itself contains a +// Protocol Error. +function decode(buffer: Uint8Array): [number, OutboundMessage] { + let compilationId: number; try { - message = OutboundMessage.deserializeBinary(buffer); + compilationId = varint.decode(buffer); } catch (error) { - throw compilerError('Invalid buffer'); - } - - let payload; - const type = message.getMessageCase(); - switch (type) { - case OutboundMessage.MessageCase.LOG_EVENT: - payload = message.getLogEvent(); - break; - case OutboundMessage.MessageCase.COMPILE_RESPONSE: - if ( - message.getCompileResponse()?.getResultCase() === - OutboundMessage.CompileResponse.ResultCase.RESULT_NOT_SET - ) { - throw compilerError( - 'OutboundMessage.CompileResponse.result is not set' - ); - } - payload = message.getCompileResponse(); - break; - case OutboundMessage.MessageCase.IMPORT_REQUEST: - payload = message.getImportRequest(); - break; - case OutboundMessage.MessageCase.FILE_IMPORT_REQUEST: - payload = message.getFileImportRequest(); - break; - case OutboundMessage.MessageCase.CANONICALIZE_REQUEST: - payload = message.getCanonicalizeRequest(); - break; - case OutboundMessage.MessageCase.FUNCTION_CALL_REQUEST: - if ( - message.getFunctionCallRequest()?.getIdentifierCase() === - OutboundMessage.FunctionCallRequest.IdentifierCase.IDENTIFIER_NOT_SET - ) { - throw compilerError( - 'OutboundMessage.FunctionCallRequest.identifier is not set' - ); - } - payload = message.getFunctionCallRequest(); - break; - case OutboundMessage.MessageCase.ERROR: - throw hostError(`${message.getError()?.getMessage()}`); - case OutboundMessage.MessageCase.MESSAGE_NOT_SET: - throw compilerError('OutboundMessage.message is not set'); - default: - throw compilerError(`Unknown message type ${message.toString()}`); + throw compilerError(`Invalid compilation ID varint: ${error}`); } - if (!payload) throw compilerError('OutboundMessage missing payload'); - return { - payload, - type, - }; -} - -// Encodes an InboundTypedMessage into a protocol buffer. -function encode(message: InboundTypedMessage): Buffer { - const inboundMessage = new InboundMessage(); - switch (message.type) { - case InboundMessage.MessageCase.COMPILE_REQUEST: - inboundMessage.setCompileRequest( - message.payload as InboundMessage.CompileRequest - ); - break; - case InboundMessage.MessageCase.IMPORT_RESPONSE: - inboundMessage.setImportResponse( - message.payload as InboundMessage.ImportResponse - ); - break; - case InboundMessage.MessageCase.FILE_IMPORT_RESPONSE: - inboundMessage.setFileImportResponse( - message.payload as InboundMessage.FileImportResponse - ); - break; - case InboundMessage.MessageCase.CANONICALIZE_RESPONSE: - inboundMessage.setCanonicalizeResponse( - message.payload as InboundMessage.CanonicalizeResponse - ); - break; - case InboundMessage.MessageCase.FUNCTION_CALL_RESPONSE: - inboundMessage.setFunctionCallResponse( - message.payload as InboundMessage.FunctionCallResponse - ); - break; + try { + return [ + compilationId, + OutboundMessage.fromBinary( + new Uint8Array(buffer.buffer, varint.decode.bytes) + ), + ]; + } catch (error) { + throw compilerError(`Invalid protobuf: ${error}`); } - return Buffer.from(inboundMessage.serializeBinary()); } diff --git a/lib/src/messages.ts b/lib/src/messages.ts new file mode 100644 index 00000000..03757ab6 --- /dev/null +++ b/lib/src/messages.ts @@ -0,0 +1,67 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import {InboundMessage, OutboundMessage} from './vendor/embedded_sass_pb'; + +// Given a message type `M` (either `InboundMessage` or `OutboundMessage`) and a +// union of possible message cases `T`, returns all the child types `M` contains +// whose cases match `T`. +type MessagesOfType = (M & { + message: {case: T; value: unknown}; +})['message']['value']; + +/** + * The names of inbound messages that are requests from the host to the + * compiler. + */ +export type InboundRequestType = 'compileRequest'; + +/** Inbound messages that are requests from the host to the compiler. */ +export type InboundRequest = MessagesOfType; + +/** + * The names of inbound of messages that are responses to `OutboundRequest`s. + */ +export type InboundResponseType = + | 'importResponse' + | 'fileImportResponse' + | 'canonicalizeResponse' + | 'functionCallResponse'; + +/** Inbound messages that are responses to `OutboundRequest`s. */ +export type InboundResponse = MessagesOfType< + InboundMessage, + InboundResponseType +>; + +/** + * The names of outbound messages that are requests from the host to the + * compiler. + */ +export type OutboundRequestType = + | 'importRequest' + | 'fileImportRequest' + | 'canonicalizeRequest' + | 'functionCallRequest'; + +/** Outbound messages that are requests from the host to the compiler. */ +export type OutboundRequest = MessagesOfType< + OutboundMessage, + OutboundRequestType +>; + +/** The names of inbound messages that are responses to `InboundRequest`s. */ +export type OutboundResponseType = 'compileResponse'; + +/** Inbound messages that are responses to `InboundRequest`s. */ +export type OutboundResponse = MessagesOfType< + OutboundMessage, + OutboundResponseType +>; + +/** The names of outbound messages that don't require responses. */ +export type OutboundEventType = 'logEvent'; + +/** Outbound messages that don't require responses. */ +export type OutboundEvent = MessagesOfType; diff --git a/lib/src/packet-transformer.ts b/lib/src/packet-transformer.ts index 2d1e8e35..0322216b 100644 --- a/lib/src/packet-transformer.ts +++ b/lib/src/packet-transformer.ts @@ -35,7 +35,7 @@ export class PacketTransformer { readonly outboundProtobufs$ = this.outboundProtobufsInternal$.pipe(); constructor( - private readonly outboundBuffers$: Observable, + private readonly outboundBuffers$: Observable, private readonly writeInboundBuffer: (buffer: Buffer) => void ) { this.outboundBuffers$ @@ -47,7 +47,7 @@ export class PacketTransformer { * Encodes a packet by pre-fixing `protobuf` with a header that describes its * length. */ - writeInboundProtobuf(protobuf: Buffer): void { + writeInboundProtobuf(protobuf: Uint8Array): void { try { let length = protobuf.length; if (length === 0) { @@ -77,7 +77,7 @@ export class PacketTransformer { // Decodes a buffer, filling up the packet that is actively being decoded. // Returns a list of decoded payloads. - private decode(buffer: Buffer): Buffer[] { + private decode(buffer: Uint8Array): Buffer[] { const payloads: Buffer[] = []; let decodedBytes = 0; while (decodedBytes < buffer.length) { @@ -124,7 +124,7 @@ class Packet { * packet. This method can be called repeatedly, incrementally building * up the packet until it is complete. */ - write(source: Buffer): number { + write(source: Uint8Array): number { if (this.isComplete) { throw Error('Cannot write to a completed Packet.'); } diff --git a/lib/src/protofier.ts b/lib/src/protofier.ts index 5ea355dc..0092cc11 100644 --- a/lib/src/protofier.ts +++ b/lib/src/protofier.ts @@ -4,7 +4,7 @@ import {OrderedMap} from 'immutable'; -import * as proto from './vendor/embedded-protocol/embedded_sass_pb'; +import * as proto from './vendor/embedded_sass_pb'; import * as utils from './utils'; import {FunctionRegistry} from './function-registry'; import {SassArgumentList} from './value/argument-list'; @@ -17,6 +17,13 @@ import {SassString} from './value/string'; import {Value} from './value'; import {sassNull} from './value/null'; import {sassTrue, sassFalse} from './value/boolean'; +import { + CalculationValue, + SassCalculation, + CalculationInterpolation, + CalculationOperation, + CalculationOperator, +} from './value/calculations'; /** * A class that converts [Value] objects into protobufs. @@ -51,74 +58,78 @@ export class Protofier { protofy(value: Value): proto.Value { const result = new proto.Value(); if (value instanceof SassString) { - const string = new proto.Value.String(); - string.setText(value.text); - string.setQuoted(value.hasQuotes); - result.setString(string); + const string = new proto.Value_String(); + string.text = value.text; + string.quoted = value.hasQuotes; + result.value = {case: 'string', value: string}; } else if (value instanceof SassNumber) { - result.setNumber(this.protofyNumber(value)); + result.value = {case: 'number', value: this.protofyNumber(value)}; } else if (value instanceof SassColor) { if (value.hasCalculatedHsl) { - const color = new proto.Value.HslColor(); - color.setHue(value.hue); - color.setSaturation(value.saturation); - color.setLightness(value.lightness); - color.setAlpha(value.alpha); - result.setHslColor(color); + const color = new proto.Value_HslColor(); + color.hue = value.hue; + color.saturation = value.saturation; + color.lightness = value.lightness; + color.alpha = value.alpha; + result.value = {case: 'hslColor', value: color}; } else { - const color = new proto.Value.RgbColor(); - color.setRed(value.red); - color.setGreen(value.green); - color.setBlue(value.blue); - color.setAlpha(value.alpha); - result.setRgbColor(color); + const color = new proto.Value_RgbColor(); + color.red = value.red; + color.green = value.green; + color.blue = value.blue; + color.alpha = value.alpha; + result.value = {case: 'rgbColor', value: color}; } } else if (value instanceof SassList) { - const list = new proto.Value.List(); - list.setSeparator(this.protofySeparator(value.separator)); - list.setHasBrackets(value.hasBrackets); + const list = new proto.Value_List(); + list.separator = this.protofySeparator(value.separator); + list.hasBrackets = value.hasBrackets; for (const element of value.asList) { - list.addContents(this.protofy(element)); + list.contents.push(this.protofy(element)); } - result.setList(list); + result.value = {case: 'list', value: list}; } else if (value instanceof SassArgumentList) { - const list = new proto.Value.ArgumentList(); - list.setId(value.id); - list.setSeparator(this.protofySeparator(value.separator)); - for (const element of value.asList) { - list.addContents(this.protofy(element)); - } - const keywords = list.getKeywordsMap(); + const list = new proto.Value_ArgumentList(); + list.id = value.id; + list.separator = this.protofySeparator(value.separator); + list.contents = value.asList + .map(element => this.protofy(element)) + .toArray(); for (const [key, mapValue] of value.keywordsInternal) { - keywords.set(key, this.protofy(mapValue)); + list.keywords[key] = this.protofy(mapValue); } - result.setArgumentList(list); + result.value = {case: 'argumentList', value: list}; } else if (value instanceof SassMap) { - const map = new proto.Value.Map(); + const map = new proto.Value_Map(); for (const [key, mapValue] of value.contents) { - const entry = new proto.Value.Map.Entry(); - entry.setKey(this.protofy(key)); - entry.setValue(this.protofy(mapValue)); - map.addEntries(entry); + const entry = new proto.Value_Map_Entry(); + entry.key = this.protofy(key); + entry.value = this.protofy(mapValue); + map.entries.push(entry); } - result.setMap(map); + result.value = {case: 'map', value: map}; } else if (value instanceof SassFunction) { if (value.id !== undefined) { - const fn = new proto.Value.CompilerFunction(); - fn.setId(value.id); - result.setCompilerFunction(fn); + const fn = new proto.Value_CompilerFunction(); + fn.id = value.id; + result.value = {case: 'compilerFunction', value: fn}; } else { - const fn = new proto.Value.HostFunction(); - fn.setId(this.functions.register(value.callback!)); - fn.setSignature(value.signature!); - result.setHostFunction(fn); + const fn = new proto.Value_HostFunction(); + fn.id = this.functions.register(value.callback!); + fn.signature = value.signature!; + result.value = {case: 'hostFunction', value: fn}; } + } else if (value instanceof SassCalculation) { + result.value = { + case: 'calculation', + value: this.protofyCalculation(value), + }; } else if (value === sassTrue) { - result.setSingleton(proto.SingletonValue.TRUE); + result.value = {case: 'singleton', value: proto.SingletonValue.TRUE}; } else if (value === sassFalse) { - result.setSingleton(proto.SingletonValue.FALSE); + result.value = {case: 'singleton', value: proto.SingletonValue.FALSE}; } else if (value === sassNull) { - result.setSingleton(proto.SingletonValue.NULL); + result.value = {case: 'singleton', value: proto.SingletonValue.NULL}; } else { throw utils.compilerError(`Unknown Value ${value}`); } @@ -126,22 +137,16 @@ export class Protofier { } /** Converts `number` to its protocol buffer representation. */ - private protofyNumber(number: SassNumber): proto.Value.Number { - const value = new proto.Value.Number(); - value.setValue(number.value); - for (const unit of number.numeratorUnits) { - value.addNumerators(unit); - } - for (const unit of number.denominatorUnits) { - value.addDenominators(unit); - } - return value; + private protofyNumber(number: SassNumber): proto.Value_Number { + return new proto.Value_Number({ + value: number.value, + numerators: number.numeratorUnits.toArray(), + denominators: number.denominatorUnits.toArray(), + }); } /** Converts `separator` to its protocol buffer representation. */ - private protofySeparator( - separator: ListSeparator - ): proto.ListSeparatorMap[keyof proto.ListSeparatorMap] { + private protofySeparator(separator: ListSeparator): proto.ListSeparator { switch (separator) { case ',': return proto.ListSeparator.COMMA; @@ -156,140 +161,196 @@ export class Protofier { } } + /** Converts `calculation` to its protocol buffer representation. */ + private protofyCalculation( + calculation: SassCalculation + ): proto.Value_Calculation { + return new proto.Value_Calculation({ + name: calculation.name, + arguments: calculation.arguments + .map(this.protofyCalculationValue.bind(this)) + .toArray(), + }); + } + + /** Converts a CalculationValue that appears within a `SassCalculation` to + * its protocol buffer representation. */ + private protofyCalculationValue( + value: Object + ): proto.Value_Calculation_CalculationValue { + const result = new proto.Value_Calculation_CalculationValue(); + if (value instanceof SassCalculation) { + result.value = { + case: 'calculation', + value: this.protofyCalculation(value), + }; + } else if (value instanceof CalculationOperation) { + result.value = { + case: 'operation', + value: new proto.Value_Calculation_CalculationOperation({ + operator: this.protofyCalculationOperator(value.operator), + left: this.protofyCalculationValue(value.left), + right: this.protofyCalculationValue(value.right), + }), + }; + } else if (value instanceof CalculationInterpolation) { + result.value = {case: 'interpolation', value: value.value}; + } else if (value instanceof SassString) { + result.value = {case: 'string', value: value.text}; + } else if (value instanceof SassNumber) { + result.value = {case: 'number', value: this.protofyNumber(value)}; + } else { + throw utils.compilerError(`Unknown CalculationValue ${value}`); + } + return result; + } + + /** Converts `operator` to its protocol buffer representation. */ + private protofyCalculationOperator( + operator: CalculationOperator + ): proto.CalculationOperator { + switch (operator) { + case '+': + return proto.CalculationOperator.PLUS; + case '-': + return proto.CalculationOperator.MINUS; + case '*': + return proto.CalculationOperator.TIMES; + case '/': + return proto.CalculationOperator.DIVIDE; + default: + throw utils.compilerError(`Unknown CalculationOperator ${operator}`); + } + } + /** Converts `value` to its JS representation. */ deprotofy(value: proto.Value): Value { - switch (value.getValueCase()) { - case proto.Value.ValueCase.STRING: { - const string = value.getString()!; - return string.getText().length === 0 - ? SassString.empty({quotes: string.getQuoted()}) - : new SassString(string.getText(), {quotes: string.getQuoted()}); + switch (value.value.case) { + case 'string': { + const string = value.value.value; + return string.text.length === 0 + ? SassString.empty({quotes: string.quoted}) + : new SassString(string.text, {quotes: string.quoted}); } - case proto.Value.ValueCase.NUMBER: - return this.deprotofyNumber(value.getNumber()!); + case 'number': { + return this.deprotofyNumber(value.value.value); + } - case proto.Value.ValueCase.RGB_COLOR: { - const color = value.getRgbColor()!; + case 'rgbColor': { + const color = value.value.value; return new SassColor({ - red: color.getRed(), - green: color.getGreen(), - blue: color.getBlue(), - alpha: color.getAlpha(), + red: color.red, + green: color.green, + blue: color.blue, + alpha: color.alpha, }); } - case proto.Value.ValueCase.HSL_COLOR: { - const color = value.getHslColor()!; + case 'hslColor': { + const color = value.value.value; return new SassColor({ - hue: color.getHue(), - saturation: color.getSaturation(), - lightness: color.getLightness(), - alpha: color.getAlpha(), + hue: color.hue, + saturation: color.saturation, + lightness: color.lightness, + alpha: color.alpha, }); } - case proto.Value.ValueCase.LIST: { - const list = value.getList()!; - const separator = this.deprotofySeparator(list.getSeparator()); + case 'list': { + const list = value.value.value; + const separator = this.deprotofySeparator(list.separator); - const contents = list.getContentsList(); - if (separator === null && contents.length > 1) { + if (separator === null && list.contents.length > 1) { throw utils.compilerError( `Value.List ${list} can't have an undecided separator because it ` + - `has ${contents.length} elements` + `has ${list.contents.length} elements` ); } return new SassList( - contents.map(element => this.deprotofy(element)), - {separator, brackets: list.getHasBrackets()} + list.contents.map(element => this.deprotofy(element)), + {separator, brackets: list.hasBrackets} ); } - case proto.Value.ValueCase.ARGUMENT_LIST: { - const list = value.getArgumentList()!; - const separator = this.deprotofySeparator(list.getSeparator()); + case 'argumentList': { + const list = value.value.value; + const separator = this.deprotofySeparator(list.separator); - const contents = list.getContentsList(); - if (separator === null && contents.length > 1) { + if (separator === null && list.contents.length > 1) { throw utils.compilerError( `Value.List ${list} can't have an undecided separator because it ` + - `has ${contents.length} elements` + `has ${list.contents.length} elements` ); } const result = new SassArgumentList( - contents.map(element => this.deprotofy(element)), + list.contents.map(element => this.deprotofy(element)), OrderedMap( - [...list.getKeywordsMap().entries()].map(([key, value]) => [ + Object.entries(list.keywords).map(([key, value]) => [ key, this.deprotofy(value), ]) ), separator, - list.getId() + list.id ); this.argumentLists.push(result); return result; } - case proto.Value.ValueCase.MAP: + case 'map': return new SassMap( OrderedMap( - value - .getMap()! - .getEntriesList() - .map(entry => { - const key = entry.getKey(); - if (!key) throw utils.mandatoryError('Value.Map.Entry.key'); - const value = entry.getValue(); - if (!value) throw utils.mandatoryError('Value.Map.Entry.value'); - - return [this.deprotofy(key), this.deprotofy(value)]; - }) + value.value.value.entries.map(entry => { + const key = entry.key; + if (!key) throw utils.mandatoryError('Value.Map.Entry.key'); + const value = entry.value; + if (!value) throw utils.mandatoryError('Value.Map.Entry.value'); + + return [this.deprotofy(key), this.deprotofy(value)]; + }) ) ); - case proto.Value.ValueCase.COMPILER_FUNCTION: - return new SassFunction(value.getCompilerFunction()!.getId()); + case 'compilerFunction': + return new SassFunction(value.value.value.id); - case proto.Value.ValueCase.HOST_FUNCTION: + case 'hostFunction': throw utils.compilerError( 'The compiler may not send Value.host_function.' ); - case proto.Value.ValueCase.SINGLETON: - switch (value.getSingleton()) { + case 'calculation': + return this.deprotofyCalculation(value.value.value); + + case 'singleton': + switch (value.value.value) { case proto.SingletonValue.TRUE: return sassTrue; case proto.SingletonValue.FALSE: return sassFalse; case proto.SingletonValue.NULL: return sassNull; - default: - throw utils.compilerError( - `Unknown Value.singleton ${value.getSingleton()}` - ); } + // eslint-disable-next-line no-fallthrough default: throw utils.mandatoryError('Value.value'); } } /** Converts `number` to its JS representation. */ - private deprotofyNumber(number: proto.Value.Number): SassNumber { - return new SassNumber(number.getValue(), { - numeratorUnits: number.getNumeratorsList(), - denominatorUnits: number.getDenominatorsList(), + private deprotofyNumber(number: proto.Value_Number): SassNumber { + return new SassNumber(number.value, { + numeratorUnits: number.numerators, + denominatorUnits: number.denominators, }); } /** Converts `separator` to its JS representation. */ - private deprotofySeparator( - separator: proto.ListSeparatorMap[keyof proto.ListSeparatorMap] - ): ListSeparator { + private deprotofySeparator(separator: proto.ListSeparator): ListSeparator { switch (separator) { case proto.ListSeparator.COMMA: return ','; @@ -303,4 +364,107 @@ export class Protofier { throw utils.compilerError(`Unknown separator ${separator}`); } } + + /** Converts `calculation` to its Sass representation. */ + private deprotofyCalculation( + calculation: proto.Value_Calculation + ): SassCalculation { + switch (calculation.name) { + case 'calc': + if (calculation.arguments.length !== 1) { + throw utils.compilerError( + 'Value.Calculation.arguments must have exactly one argument for calc().' + ); + } + return SassCalculation.calc( + this.deprotofyCalculationValue(calculation.arguments[0]) + ); + case 'clamp': + if ( + calculation.arguments.length === 0 || + calculation.arguments.length > 3 + ) { + throw utils.compilerError( + 'Value.Calculation.arguments must have 1 to 3 arguments for clamp().' + ); + } + return SassCalculation.clamp( + this.deprotofyCalculationValue(calculation.arguments[0]), + calculation.arguments.length > 1 + ? this.deprotofyCalculationValue(calculation.arguments[1]) + : undefined, + calculation.arguments.length > 2 + ? this.deprotofyCalculationValue(calculation.arguments[2]) + : undefined + ); + case 'min': + if (calculation.arguments.length === 0) { + throw utils.compilerError( + 'Value.Calculation.arguments must have at least 1 argument for min().' + ); + } + return SassCalculation.min( + calculation.arguments.map(this.deprotofyCalculationValue) + ); + case 'max': + if (calculation.arguments.length === 0) { + throw utils.compilerError( + 'Value.Calculation.arguments must have at least 1 argument for max().' + ); + } + return SassCalculation.max( + calculation.arguments.map(this.deprotofyCalculationValue) + ); + default: + throw utils.compilerError( + `Value.Calculation.name "${calculation.name}" is not a recognized calculation type.` + ); + } + } + + /** Converts `value` to its Sass representation. */ + private deprotofyCalculationValue( + value: proto.Value_Calculation_CalculationValue + ): CalculationValue { + switch (value.value.case) { + case 'number': + return this.deprotofyNumber(value.value.value); + case 'calculation': + return this.deprotofyCalculation(value.value.value); + case 'string': + return new SassString(value.value.value, {quotes: false}); + case 'operation': + return new CalculationOperation( + this.deprotofyCalculationOperator(value.value.value.operator), + this.deprotofyCalculationValue( + value.value.value.left as proto.Value_Calculation_CalculationValue + ), + this.deprotofyCalculationValue( + value.value.value.right as proto.Value_Calculation_CalculationValue + ) + ); + case 'interpolation': + return new CalculationInterpolation(value.value.value); + default: + throw utils.mandatoryError('Calculation.CalculationValue.value'); + } + } + + /** Converts `operator` to its Sass representation. */ + private deprotofyCalculationOperator( + operator: proto.CalculationOperator + ): CalculationOperator { + switch (operator) { + case proto.CalculationOperator.PLUS: + return '+'; + case proto.CalculationOperator.MINUS: + return '-'; + case proto.CalculationOperator.TIMES: + return '*'; + case proto.CalculationOperator.DIVIDE: + return '/'; + default: + throw utils.compilerError(`Unknown CalculationOperator ${operator}`); + } + } } diff --git a/lib/src/request-tracker.test.ts b/lib/src/request-tracker.test.ts index e74501e6..dd90cba3 100644 --- a/lib/src/request-tracker.test.ts +++ b/lib/src/request-tracker.test.ts @@ -2,10 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import { - InboundMessage, - OutboundMessage, -} from './vendor/embedded-protocol/embedded_sass_pb'; import {RequestTracker} from './request-tracker'; describe('request tracker', () => { @@ -21,74 +17,73 @@ describe('request tracker', () => { describe('tracking requests', () => { it('tracks when empty', () => { - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); + tracker.add(0, 'compileResponse'); expect(tracker.nextId).toBe(1); }); it('tracks multiple requests', () => { - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.add(1, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.add(2, OutboundMessage.MessageCase.COMPILE_RESPONSE); + tracker.add(0, 'compileResponse'); + tracker.add(1, 'compileResponse'); + tracker.add(2, 'compileResponse'); expect(tracker.nextId).toBe(3); }); it('tracks starting from a non-zero ID', () => { - tracker.add(1, OutboundMessage.MessageCase.COMPILE_RESPONSE); + tracker.add(1, 'compileResponse'); expect(tracker.nextId).toBe(0); - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); + tracker.add(0, 'compileResponse'); expect(tracker.nextId).toBe(2); }); it('errors if the request ID is invalid', () => { - expect(() => - tracker.add(-1, OutboundMessage.MessageCase.COMPILE_RESPONSE) - ).toThrowError('Invalid request ID -1.'); + expect(() => tracker.add(-1, 'compileResponse')).toThrowError( + 'Invalid request ID -1.' + ); }); it('errors if the request ID overlaps that of an existing in-flight request', () => { - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); - expect(() => - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE) - ).toThrowError('Request ID 0 is already in use by an in-flight request.'); + tracker.add(0, 'compileResponse'); + expect(() => tracker.add(0, 'compileResponse')).toThrowError( + 'Request ID 0 is already in use by an in-flight request.' + ); }); }); describe('resolving requests', () => { it('resolves a single request', () => { - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.resolve(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); + tracker.add(0, 'compileResponse'); + tracker.resolve(0, 'compileResponse'); expect(tracker.nextId).toBe(0); }); it('resolves multiple requests', () => { - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.add(1, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.add(2, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.resolve(1, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.resolve(2, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.resolve(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); + tracker.add(0, 'compileResponse'); + tracker.add(1, 'compileResponse'); + tracker.add(2, 'compileResponse'); + tracker.resolve(1, 'compileResponse'); + tracker.resolve(2, 'compileResponse'); + tracker.resolve(0, 'compileResponse'); expect(tracker.nextId).toBe(0); }); it('reuses the ID of a resolved request', () => { - tracker.add(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.add(1, OutboundMessage.MessageCase.COMPILE_RESPONSE); - tracker.resolve(0, OutboundMessage.MessageCase.COMPILE_RESPONSE); + tracker.add(0, 'compileResponse'); + tracker.add(1, 'compileResponse'); + tracker.resolve(0, 'compileResponse'); expect(tracker.nextId).toBe(0); }); it('errors if the response ID does not match any existing request IDs', () => { - expect(() => - tracker.resolve(0, OutboundMessage.MessageCase.COMPILE_RESPONSE) - ).toThrowError('Response ID 0 does not match any pending requests.'); + expect(() => tracker.resolve(0, 'compileResponse')).toThrowError( + 'Response ID 0 does not match any pending requests.' + ); }); it('errors if the response type does not match what the request is expecting', () => { - tracker.add(0, InboundMessage.MessageCase.IMPORT_RESPONSE); - expect(() => - tracker.resolve(0, InboundMessage.MessageCase.FILE_IMPORT_RESPONSE) - ).toThrowError( - "Response with ID 0 does not match pending request's type. Expected 4 but received 5." + tracker.add(0, 'importResponse'); + expect(() => tracker.resolve(0, 'fileImportResponse')).toThrowError( + "Response with ID 0 does not match pending request's type. Expected " + + 'importResponse but received fileImportResponse.' ); }); }); diff --git a/lib/src/request-tracker.ts b/lib/src/request-tracker.ts index 5c8cbbbf..f26db4ca 100644 --- a/lib/src/request-tracker.ts +++ b/lib/src/request-tracker.ts @@ -2,7 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import {InboundResponseType, OutboundResponseType} from './message-transformer'; +import {InboundResponseType, OutboundResponseType} from './messages'; /** * Manages pending inbound and outbound requests. Ensures that requests and diff --git a/lib/src/sync-compiler.ts b/lib/src/sync-compiler.ts index 4aec442a..2950fbdc 100644 --- a/lib/src/sync-compiler.ts +++ b/lib/src/sync-compiler.ts @@ -5,7 +5,7 @@ import {Subject} from 'rxjs'; import {SyncProcess} from './sync-process'; -import {compilerPath} from './compiler-path'; +import {compilerCommand} from './compiler-path'; /** * A synchronous wrapper for the embedded Sass compiler that exposes its stdio @@ -13,7 +13,11 @@ import {compilerPath} from './compiler-path'; */ export class SyncEmbeddedCompiler { /** The underlying process that's being wrapped. */ - private readonly process = new SyncProcess(compilerPath, {windowsHide: true}); + private readonly process = new SyncProcess( + compilerCommand[0], + [...compilerCommand.slice(1), '--embedded'], + {windowsHide: true} + ); /** The buffers emitted by the child process's stdout. */ readonly stdout$ = new Subject(); diff --git a/lib/src/utils.ts b/lib/src/utils.ts index ef108bf9..8b33b1f5 100644 --- a/lib/src/utils.ts +++ b/lib/src/utils.ts @@ -6,7 +6,7 @@ import {List} from 'immutable'; import * as p from 'path'; import * as url from 'url'; -import * as proto from './vendor/embedded-protocol/embedded_sass_pb'; +import * as proto from './vendor/embedded_sass_pb'; import {Syntax} from './vendor/sass'; export type PromiseOr< @@ -121,9 +121,7 @@ export function withoutExtension(path: string): string { } /** Converts a JS syntax string into a protobuf syntax enum. */ -export function protofySyntax( - syntax: Syntax -): proto.SyntaxMap[keyof proto.SyntaxMap] { +export function protofySyntax(syntax: Syntax): proto.Syntax { switch (syntax) { case 'scss': return proto.Syntax.SCSS; diff --git a/lib/src/value/calculations.ts b/lib/src/value/calculations.ts new file mode 100644 index 00000000..aa160e67 --- /dev/null +++ b/lib/src/value/calculations.ts @@ -0,0 +1,137 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import {hash, List, ValueObject} from 'immutable'; + +import {Value} from './index'; +import {SassNumber} from './number'; +import {SassString} from './string'; + +export type CalculationValue = + | SassNumber + | SassCalculation + | SassString + | CalculationOperation + | CalculationInterpolation; + +type CalculationValueIterable = CalculationValue[] | List; + +function assertCalculationValue(value: CalculationValue): void { + if (value instanceof SassString && value.hasQuotes) { + throw new Error(`Expected ${value} to be an unquoted string.`); + } +} + +const isValidClampArg = (value: CalculationValue): boolean => + value instanceof CalculationInterpolation || + (value instanceof SassString && !value.hasQuotes); + +/* A SassScript calculation */ +export class SassCalculation extends Value { + readonly arguments: List; + + private constructor(readonly name: string, args: CalculationValueIterable) { + super(); + this.arguments = List(args); + } + + static calc(argument: CalculationValue): SassCalculation { + assertCalculationValue(argument); + return new SassCalculation('calc', [argument]); + } + + static min(args: CalculationValueIterable): SassCalculation { + args.forEach(assertCalculationValue); + return new SassCalculation('min', args); + } + + static max(args: CalculationValueIterable): SassCalculation { + args.forEach(assertCalculationValue); + return new SassCalculation('max', args); + } + + static clamp( + min: CalculationValue, + value?: CalculationValue, + max?: CalculationValue + ): SassCalculation { + if ( + (value === undefined && !isValidClampArg(min)) || + (max === undefined && ![min, value].some(x => x && isValidClampArg(x))) + ) { + throw new Error( + 'Argument must be an unquoted SassString or CalculationInterpolation.' + ); + } + const args = [min]; + if (value !== undefined) args.push(value); + if (max !== undefined) args.push(max); + args.forEach(assertCalculationValue); + return new SassCalculation('clamp', args); + } + + assertCalculation(): SassCalculation { + return this; + } + + equals(other: unknown): boolean { + return ( + other instanceof SassCalculation && + this.name === other.name && + this.arguments.equals(other.arguments) + ); + } + + hashCode(): number { + return hash(this.name) ^ this.arguments.hashCode(); + } + + toString(): string { + return `${this.name}(${this.arguments.join(', ')})`; + } +} + +const operators = ['+', '-', '*', '/'] as const; +export type CalculationOperator = typeof operators[number]; + +export class CalculationOperation implements ValueObject { + constructor( + readonly operator: CalculationOperator, + readonly left: CalculationValue, + readonly right: CalculationValue + ) { + if (!operators.includes(operator)) { + throw new Error(`Invalid operator: ${operator}`); + } + assertCalculationValue(left); + assertCalculationValue(right); + } + + equals(other: unknown): boolean { + return ( + other instanceof CalculationOperation && + this.operator === other.operator && + this.left === other.left && + this.right === other.right + ); + } + + hashCode(): number { + return hash(this.operator) ^ hash(this.left) ^ hash(this.right); + } +} + +export class CalculationInterpolation implements ValueObject { + constructor(readonly value: string) {} + + equals(other: unknown): boolean { + return ( + other instanceof CalculationInterpolation && this.value === other.value + ); + } + + hashCode(): number { + return hash(this.value); + } +} diff --git a/lib/src/value/index.ts b/lib/src/value/index.ts index 536eab64..d7c95f8e 100644 --- a/lib/src/value/index.ts +++ b/lib/src/value/index.ts @@ -11,6 +11,7 @@ import {SassMap} from './map'; import {SassNumber} from './number'; import {SassString} from './string'; import {valueError} from '../utils'; +import {SassCalculation} from './calculations'; /** * A SassScript value. @@ -106,6 +107,16 @@ export abstract class Value implements ValueObject { throw valueError(`${this} is not a boolean`, name); } + /** + * Casts `this` to `SassCalculation`; throws if `this` isn't a calculation. + * + * If `this` came from a function argument, `name` is the argument name + * (without the `$`) and is used for error reporting. + */ + assertCalculation(name?: string): SassCalculation { + throw valueError(`${this} is not a calculation`, name); + } + /** * Casts `this` to `SassColor`; throws if `this` isn't a color. * diff --git a/npm/darwin-arm64/package.json b/npm/darwin-arm64/package.json index 21273c19..4bde48d2 100644 --- a/npm/darwin-arm64/package.json +++ b/npm/darwin-arm64/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-darwin-arm64", - "version": "1.57.1", + "version": "1.64.2", "description": "The darwin-arm64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass" + }, "os": [ "darwin" ], diff --git a/npm/darwin-x64/package.json b/npm/darwin-x64/package.json index a8fd6f06..2531813d 100644 --- a/npm/darwin-x64/package.json +++ b/npm/darwin-x64/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-darwin-x64", - "version": "1.57.1", + "version": "1.64.2", "description": "The darwin-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass" + }, "os": [ "darwin" ], diff --git a/npm/linux-arm/package.json b/npm/linux-arm/package.json index 13f8c4a5..42779010 100644 --- a/npm/linux-arm/package.json +++ b/npm/linux-arm/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-linux-arm", - "version": "1.57.1", + "version": "1.64.2", "description": "The linux-arm binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass" + }, "os": [ "linux" ], diff --git a/npm/linux-arm64/package.json b/npm/linux-arm64/package.json index 5dea4da0..d5d17040 100644 --- a/npm/linux-arm64/package.json +++ b/npm/linux-arm64/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-linux-arm64", - "version": "1.57.1", + "version": "1.64.2", "description": "The linux-arm64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass" + }, "os": [ "linux" ], diff --git a/npm/linux-ia32/package.json b/npm/linux-ia32/package.json index 5ea3d9ab..b334ab9d 100644 --- a/npm/linux-ia32/package.json +++ b/npm/linux-ia32/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-linux-ia32", - "version": "1.57.1", + "version": "1.64.2", "description": "The linux-ia32 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass" + }, "os": [ "linux" ], diff --git a/npm/linux-x64/package.json b/npm/linux-x64/package.json index aa82361b..5bb0f833 100644 --- a/npm/linux-x64/package.json +++ b/npm/linux-x64/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-linux-x64", - "version": "1.57.1", + "version": "1.64.2", "description": "The linux-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass" + }, "os": [ "linux" ], diff --git a/npm/win32-ia32/package.json b/npm/win32-ia32/package.json index 0a257b66..8620225d 100644 --- a/npm/win32-ia32/package.json +++ b/npm/win32-ia32/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-win32-ia32", - "version": "1.57.1", + "version": "1.64.2", "description": "The win32-ia32 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass.bat" + }, "os": [ "win32" ], diff --git a/npm/win32-x64/package.json b/npm/win32-x64/package.json index 8392b3ff..7ae36f8c 100644 --- a/npm/win32-x64/package.json +++ b/npm/win32-x64/package.json @@ -1,16 +1,19 @@ { "name": "sass-embedded-win32-x64", - "version": "1.57.1", + "version": "1.64.2", "description": "The win32-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", "files": [ - "dart-sass-embedded/**/*" + "dart-sass/**/*" ], "engines": { "node": ">=14.0.0" }, + "bin": { + "sass": "./dart-sass/sass.bat" + }, "os": [ "win32" ], diff --git a/package.json b/package.json index ca41601e..820be1cc 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,16 @@ { "name": "sass-embedded", - "version": "1.57.1", - "protocol-version": "1.2.0", - "compiler-version": "1.58.0-dev", + "version": "1.64.2", + "protocol-version": "2.1.0", + "compiler-version": "1.64.2", "description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol", "repository": "sass/embedded-host-node", "author": "Google Inc.", "license": "MIT", + "exports": { + "import": "./dist/lib/index.mjs", + "default": "./dist/lib/index.js" + }, "main": "dist/lib/index.js", "types": "dist/types/index.d.ts", "files": [ @@ -21,55 +25,55 @@ "check:gts": "gts check", "check:tsc": "tsc --noEmit", "clean": "gts clean", - "compile": "tsc", + "compile": "tsc && cp lib/index.mjs dist/lib/index.mjs", "fix": "gts fix", "prepublishOnly": "npm run clean && ts-node ./tool/prepare-release.ts", "test": "jest" }, "optionalDependencies": { - "sass-embedded-darwin-arm64": "1.57.1", - "sass-embedded-darwin-x64": "1.57.1", - "sass-embedded-linux-arm": "1.57.1", - "sass-embedded-linux-arm64": "1.57.1", - "sass-embedded-linux-ia32": "1.57.1", - "sass-embedded-linux-x64": "1.57.1", - "sass-embedded-win32-ia32": "1.57.1", - "sass-embedded-win32-x64": "1.57.1" + "sass-embedded-darwin-arm64": "1.64.2", + "sass-embedded-darwin-x64": "1.64.2", + "sass-embedded-linux-arm": "1.64.2", + "sass-embedded-linux-arm64": "1.64.2", + "sass-embedded-linux-ia32": "1.64.2", + "sass-embedded-linux-x64": "1.64.2", + "sass-embedded-win32-ia32": "1.64.2", + "sass-embedded-win32-x64": "1.64.2" }, "dependencies": { + "@bufbuild/protobuf": "^1.0.0", "buffer-builder": "^0.2.0", - "google-protobuf": "^3.11.4", "immutable": "^4.0.0", "rxjs": "^7.4.0", - "supports-color": "^8.1.1" + "supports-color": "^8.1.1", + "varint": "^6.0.0" }, "devDependencies": { + "@bufbuild/buf": "^1.13.1-4", + "@bufbuild/protoc-gen-es": "^1.0.0", "@types/buffer-builder": "^0.2.0", "@types/google-protobuf": "^3.7.2", - "@types/jest": "^27.0.2", - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.0", + "@types/jest": "^29.4.0", + "@types/node": "^20.1.0", "@types/shelljs": "^0.8.8", "@types/supports-color": "^8.1.1", "@types/tar": "^6.1.0", + "@types/varint": "^6.0.1", "@types/yargs": "^17.0.4", "del": "^6.0.0", "extract-zip": "^2.0.1", "gts": "^4.0.0", - "jest": "^27.2.5", - "minipass": "3.2.1", - "node-fetch": "^2.6.0", + "jest": "^29.4.1", + "minipass": "7.0.2", "npm-run-all": "^4.1.5", - "protoc": "1.0.4", "shelljs": "^0.8.4", "simple-git": "^3.15.1", "source-map-js": "^1.0.2", "tar": "^6.0.5", - "ts-jest": "^27.0.5", + "ts-jest": "^29.0.5", "ts-node": "^10.2.1", - "ts-protoc-gen": "^0.15.0", - "typescript": "^4.4.3", - "yaml": "^1.10.2", + "typescript": "^5.0.2", + "yaml": "^2.2.1", "yargs": "^17.2.1" } } diff --git a/test/after-compile-test.mjs b/test/after-compile-test.mjs new file mode 100644 index 00000000..805cbca6 --- /dev/null +++ b/test/after-compile-test.mjs @@ -0,0 +1,33 @@ +// Copyright 2023 Google LLC. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import * as fs from 'fs'; + +// Note: this file isn't .test.ts specifically because we _don't_ want Jest to +// handle it, because Jest chokes on dynamic imports of literal ESM modules. + +// This file should only be run _after_ `npm run compile`. +if (!fs.existsSync('dist/package.json')) { + throw new Error('after-compile.test.ts must be run after `npm run compile`.'); +} + +// Load these dynamically so we have a better error mesage if `npm run compile` +// hasn't been run. +const cjs = await import('../dist/lib/index.js'); +const esm = await import('../dist/lib/index.mjs'); + +for (const [name, value] of Object.entries(cjs)) { + if (name === '__esModule' || name === 'default') continue; + if (!esm[name]) { + throw new Error(`ESM module is missing export ${name}.`); + } else if (esm[name] !== value) { + throw new Error(`ESM ${name} isn't the same as CJS.`); + } + + if (!esm.default[name]) { + throw new Error(`ESM default export is missing export ${name}.`); + } else if (esm.default[name] !== value) { + throw new Error(`ESM default export ${name} isn't the same as CJS.`); + } +} diff --git a/test/dependencies.test.ts b/test/dependencies.test.ts index e3d6746c..e29f547a 100644 --- a/test/dependencies.test.ts +++ b/test/dependencies.test.ts @@ -2,16 +2,13 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import * as child_process from 'child_process'; import * as fs from 'fs'; import * as p from 'path'; import * as pkg from '../package.json'; -// These tests assert that our declared dependencies on the embedded protocol -// and compiler are either -dev versions (which download the latest main -// branches of each repo and block release) or the same versions as the versions -// we're testing against. +// These tests assert that our declared dependency on the embedded protocol is +// either a -dev version or the same version we're testing against. it('declares a compatible dependency on the embedded protocol', () => { if (pkg['protocol-version'].endsWith('-dev')) return; @@ -19,24 +16,9 @@ it('declares a compatible dependency on the embedded protocol', () => { expect( fs .readFileSync( - p.join(__dirname, '../lib/src/vendor/embedded-protocol/VERSION'), + p.join(__dirname, '../build/sass/spec/EMBEDDED_PROTOCOL_VERSION'), 'utf-8' ) .trim() ).toBe(pkg['protocol-version']); }); - -it('declares a compatible dependency on the embedded compiler', () => { - if (pkg['compiler-version'].endsWith('-dev')) return; - - const version = JSON.parse( - child_process.execSync( - p.join( - __dirname, - '../lib/src/vendor/dart-sass-embedded/dart-sass-embedded' - ) + ' --version', - {encoding: 'utf-8'} - ) - ); - expect(version.compilerVersion).toBe(pkg['compiler-version']); -}); diff --git a/test/utils.ts b/test/utils.ts index 1634b5ec..f8733917 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -15,12 +15,16 @@ export function expectObservableToError( done: () => void ): void { observable.subscribe({ - next: () => fail('expected error'), + next: () => { + throw new Error('expected error'); + }, error: error => { expect(error.message).toBe(errorMessage); done(); }, - complete: () => fail('expected error'), + complete: () => { + throw new Error('expected error'); + }, }); } diff --git a/tool/get-embedded-compiler.ts b/tool/get-embedded-compiler.ts index 239c97ab..2b702a2a 100644 --- a/tool/get-embedded-compiler.ts +++ b/tool/get-embedded-compiler.ts @@ -2,9 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import {promises as fs} from 'fs'; import * as p from 'path'; -import * as yaml from 'yaml'; import * as shell from 'shelljs'; import * as utils from './utils'; @@ -19,52 +17,45 @@ export async function getEmbeddedCompiler( outPath: string, options?: {ref: string} | {path: string} ): Promise { - const repo = 'dart-sass-embedded'; + const repo = 'dart-sass'; let source: string; if (!options || 'ref' in options) { utils.fetchRepo({ repo, - outPath: utils.BUILD_PATH, + outPath: 'build', ref: options?.ref ?? 'main', }); - source = p.join(utils.BUILD_PATH, repo); - await maybeOverrideSassDependency(source); + source = p.join('build', repo); } else { source = options.path; } + // Make sure the compiler sees the same version of the language repo that the + // host is using, but if they're already the same directory (as in the Dart + // Sass CI environment) we don't need to do anything. + const languageInHost = p.resolve('build/sass'); + const languageInCompiler = p.resolve(p.join(source, 'build/language')); + if (!(await utils.sameTarget(languageInHost, languageInCompiler))) { + await utils.cleanDir(languageInCompiler); + await utils.link(languageInHost, languageInCompiler); + } + buildDartSassEmbedded(source); await utils.link(p.join(source, 'build'), p.join(outPath, repo)); } -/** - * Overrides Embedded Dart Sass compiler's dependency on Dart Sass to use the - * latest version of Dart Sass from the `main` branch. - * - * This allows us to avoid needing to commit a dependency override to the - * embedded compiler when it doesn't actually require any local changes. - */ -async function maybeOverrideSassDependency(repo: string): Promise { - const pubspecPath = p.join(repo, 'pubspec.yaml'); - const pubspec = yaml.parse( - await fs.readFile(pubspecPath, {encoding: 'utf-8'}) - ); - - console.log(`Overriding ${repo} to load Dart Sass from HEAD.`); - - pubspec['dependency_overrides'] = { - ...pubspec['dependency_overrides'], - sass: {git: 'https://github.com/sass/dart-sass.git'}, - }; - await fs.writeFile(pubspecPath, yaml.stringify(pubspec), {encoding: 'utf-8'}); -} - // Builds the Embedded Dart Sass executable from the source at `repoPath`. function buildDartSassEmbedded(repoPath: string): void { - console.log('Downloading dart-sass-embedded dependencies.'); - shell.exec('dart pub upgrade', {cwd: repoPath}); - - console.log('Building dart-sass-embedded executable.'); - shell.exec('dart run grinder protobuf pkg-standalone-dev', {cwd: repoPath}); + console.log("Downloading Dart Sass's dependencies."); + shell.exec('dart pub upgrade', { + cwd: repoPath, + silent: true, + }); + + console.log('Building the Dart Sass executable.'); + shell.exec('dart run grinder protobuf pkg-standalone-dev', { + cwd: repoPath, + env: {...process.env, UPDATE_SASS_PROTOCOL: 'false'}, + }); } diff --git a/tool/get-embedded-protocol.ts b/tool/get-embedded-protocol.ts deleted file mode 100644 index 9f34e390..00000000 --- a/tool/get-embedded-protocol.ts +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2022 Google Inc. Use of this source code is governed by an -// MIT-style license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -import {mkdirSync} from 'fs'; -import * as p from 'path'; -import * as shell from 'shelljs'; - -import * as pkg from '../package.json'; -import * as utils from './utils'; - -/** - * Downloads and builds the Embedded Sass protocol definition. - * - * Can check out and build the source from a Git `ref` or build from the source - * at `path`. By default, checks out the tagged version specified in - * package.json's `protocol-version` field. If this version ends in `-dev`, - * checks out the latest revision from GitHub instead. - */ -export async function getEmbeddedProtocol( - outPath: string, - options?: {ref: string} | {path: string} -): Promise { - const repo = 'embedded-protocol'; - - let source: string; - if (!options || 'ref' in options) { - let ref = options?.ref; - if (ref === undefined) { - const version = pkg['protocol-version'] as string; - ref = version.endsWith('-dev') ? 'main' : version; - } - - utils.fetchRepo({repo, outPath: utils.BUILD_PATH, ref}); - source = p.join(utils.BUILD_PATH, repo); - } else { - source = options.path; - } - - buildEmbeddedProtocol(source); - - // Make the VERSION consistently accessible for the dependency test and any - // curious users. - await utils.link( - p.join(source, 'VERSION'), - 'build/embedded-protocol-out/VERSION' - ); - await utils.link('build/embedded-protocol-out', p.join(outPath, repo)); -} - -// Builds the embedded proto at `repoPath` into a pbjs with TS declaration file. -function buildEmbeddedProtocol(repoPath: string): void { - const proto = p.join(repoPath, 'embedded_sass.proto'); - const protocPath = - process.platform === 'win32' - ? '%CD%/node_modules/protoc/protoc/bin/protoc.exe' - : 'node_modules/protoc/protoc/bin/protoc'; - const version = shell - .exec(`${protocPath} --version`, {silent: true}) - .stdout.trim(); - console.log( - `Building pbjs and TS declaration file from ${proto} with ${version}.` - ); - - const pluginPath = - process.platform === 'win32' - ? '%CD%/node_modules/.bin/protoc-gen-ts.cmd' - : 'node_modules/.bin/protoc-gen-ts'; - mkdirSync('build/embedded-protocol-out', {recursive: true}); - shell.exec( - `${protocPath} \ - --plugin="protoc-gen-ts=${pluginPath}" \ - --js_out="import_style=commonjs,binary:build/embedded-protocol-out" \ - --ts_out="build/embedded-protocol-out" \ - --proto_path="${repoPath}" \ - ${proto}` - ); -} diff --git a/tool/get-js-api.ts b/tool/get-js-api.ts deleted file mode 100644 index adf73070..00000000 --- a/tool/get-js-api.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2022 Google Inc. Use of this source code is governed by an -// MIT-style license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -import * as p from 'path'; - -import * as utils from './utils'; - -/** - * Checks out JS API type definitions from the Sass language repo. - * - * Can check out a Git `ref` or link to the source at `path`. By default, checks - * out the latest revision from GitHub. - */ -export async function getJSApi( - outPath: string, - options?: {ref: string} | {path: string} -): Promise { - const repo = 'sass'; - - let source: string; - if (!options || 'ref' in options) { - utils.fetchRepo({ - repo, - outPath: utils.BUILD_PATH, - ref: options?.ref ?? 'main', - }); - source = p.join(utils.BUILD_PATH, repo); - } else { - source = options.path; - } - - await utils.link(p.join(source, 'js-api-doc'), p.join(outPath, repo)); -} diff --git a/tool/get-language-repo.ts b/tool/get-language-repo.ts new file mode 100644 index 00000000..83a264b8 --- /dev/null +++ b/tool/get-language-repo.ts @@ -0,0 +1,47 @@ +// Copyright 2022 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import * as p from 'path'; +import * as shell from 'shelljs'; + +import * as utils from './utils'; + +/** + * Downloads the Sass language repo and buids the Embedded Sass protocol + * definition. + * + * Can check out and build the source from a Git `ref` or build from the source + * at `path`. By default, checks out the latest revision from GitHub. + */ +export async function getLanguageRepo( + outPath: string, + options?: {ref: string} | {path: string} +): Promise { + if (!options || 'ref' in options) { + utils.fetchRepo({ + repo: 'sass', + outPath: utils.BUILD_PATH, + ref: options?.ref ?? 'main', + }); + } else { + await utils.cleanDir('build/sass'); + await utils.link(options.path, 'build/sass'); + } + + // Workaround for https://github.com/shelljs/shelljs/issues/198 + // This file is a symlink which gets messed up by `shell.cp` (called from + // `utils.link`) on Windows. + shell.rm('build/sass/spec/README.md'); + + await utils.link('build/sass/js-api-doc', p.join(outPath, 'sass')); + + buildEmbeddedProtocol(); +} + +// Builds the embedded proto into a TS file. +function buildEmbeddedProtocol(): void { + const version = shell.exec('npx buf --version', {silent: true}).stdout.trim(); + console.log(`Building TS with buf ${version}.`); + shell.exec('npx buf generate'); +} diff --git a/tool/init.ts b/tool/init.ts index 2106a62e..cb50c12b 100644 --- a/tool/init.ts +++ b/tool/init.ts @@ -5,8 +5,7 @@ import yargs from 'yargs'; import {getEmbeddedCompiler} from './get-embedded-compiler'; -import {getEmbeddedProtocol} from './get-embedded-protocol'; -import {getJSApi} from './get-js-api'; +import {getLanguageRepo} from './get-language-repo'; const argv = yargs(process.argv.slice(2)) .option('compiler-path', { @@ -22,27 +21,18 @@ const argv = yargs(process.argv.slice(2)) type: 'boolean', description: "Don't Embedded Dart Sass at all.", }) - .option('protocol-path', { + .option('language-path', { type: 'string', - description: 'Build the Embedded Protocol from the source at this path.', + description: 'Use the Sass language repo from the source at this path.', }) - .option('protocol-ref', { + .option('language-ref', { type: 'string', - description: 'Build the Embedded Protocol from this Git ref.', - }) - .option('api-path', { - type: 'string', - description: 'Use the JS API definitions from the source at this path.', - }) - .option('api-ref', { - type: 'string', - description: 'Build the JS API definitions from this Git ref.', + description: 'Use the Sass language repo from this Git ref.', }) .conflicts({ 'compiler-path': ['compiler-ref', 'skip-compiler'], 'compiler-ref': ['skip-compiler'], - 'protocol-path': ['protocol-ref'], - 'api-path': 'api-ref', + 'language-path': ['language-ref'], }) .parseSync(); @@ -50,16 +40,16 @@ const argv = yargs(process.argv.slice(2)) try { const outPath = 'lib/src/vendor'; - if (argv['protocol-ref']) { - await getEmbeddedProtocol(outPath, { - ref: argv['protocol-ref'], + if (argv['language-ref']) { + await getLanguageRepo(outPath, { + ref: argv['language-ref'], }); - } else if (argv['protocol-path']) { - await getEmbeddedProtocol(outPath, { - path: argv['protocol-path'], + } else if (argv['language-path']) { + await getLanguageRepo(outPath, { + path: argv['language-path'], }); } else { - await getEmbeddedProtocol(outPath); + await getLanguageRepo(outPath); } if (!argv['skip-compiler']) { @@ -75,18 +65,6 @@ const argv = yargs(process.argv.slice(2)) await getEmbeddedCompiler(outPath); } } - - if (argv['api-ref']) { - await getJSApi(outPath, { - ref: argv['api-ref'], - }); - } else if (argv['api-path']) { - await getJSApi(outPath, { - path: argv['api-path'], - }); - } else { - await getJSApi(outPath); - } } catch (error) { console.error(error); process.exitCode = 1; diff --git a/tool/prepare-optional-release.ts b/tool/prepare-optional-release.ts index 1847d1fd..9ad7cb68 100644 --- a/tool/prepare-optional-release.ts +++ b/tool/prepare-optional-release.ts @@ -1,6 +1,5 @@ import extractZip = require('extract-zip'); import {promises as fs} from 'fs'; -import fetch from 'node-fetch'; import * as p from 'path'; import {extract as extractTar} from 'tar'; import yargs from 'yargs'; @@ -80,7 +79,7 @@ async function downloadRelease(options: { `Failed to download ${options.repo} release asset: ${response.statusText}` ); } - const releaseAsset = await response.buffer(); + const releaseAsset = Buffer.from(await response.arrayBuffer()); console.log(`Unzipping ${options.repo} release asset to ${options.outPath}.`); await utils.cleanDir(p.join(options.outPath, options.repo)); @@ -119,17 +118,13 @@ async function downloadRelease(options: { const dartArch = nodeArchToDartArch(nodeArch); const outPath = p.join('npm', argv.package); await downloadRelease({ - repo: 'dart-sass-embedded', + repo: 'dart-sass', assetUrl: - 'https://github.com/sass/dart-sass-embedded/releases/download/' + - `${version}/sass_embedded-${version}-` + + 'https://github.com/sass/dart-sass/releases/download/' + + `${version}/dart-sass-${version}-` + `${dartPlatform}-${dartArch}${getArchiveExtension(dartPlatform)}`, outPath, }); - await fs.rename( - p.join(outPath, 'sass_embedded'), - p.join(outPath, 'dart-sass-embedded') - ); } catch (error) { console.error(error); process.exitCode = 1; diff --git a/tool/prepare-release.ts b/tool/prepare-release.ts index 20de61d8..8d4c08b4 100644 --- a/tool/prepare-release.ts +++ b/tool/prepare-release.ts @@ -6,29 +6,22 @@ import {promises as fs} from 'fs'; import * as shell from 'shelljs'; import * as pkg from '../package.json'; -import {getEmbeddedProtocol} from './get-embedded-protocol'; -import {getJSApi} from './get-js-api'; +import {getLanguageRepo} from './get-language-repo'; (async () => { try { await sanityCheckBeforeRelease(); - await getEmbeddedProtocol('lib/src/vendor'); - - await getJSApi('lib/src/vendor'); + await getLanguageRepo('lib/src/vendor'); console.log('Transpiling TS into dist.'); shell.exec('tsc'); + shell.cp('lib/index.mjs', 'dist/lib/index.mjs'); console.log('Copying JS API types to dist.'); shell.cp('-R', 'lib/src/vendor/sass', 'dist/types'); await fs.unlink('dist/types/README.md'); - // .gitignore needs to exist in dist for `npm publish` to correctly exclude - // files from the published tarball. - console.log('Copying .gitignore to dist.'); - await fs.copyFile('.gitignore', 'dist/.gitignore'); - console.log('Ready for publishing to npm.'); } catch (error) { console.error(error); diff --git a/tool/utils.ts b/tool/utils.ts index 7d1b339f..516b5a06 100644 --- a/tool/utils.ts +++ b/tool/utils.ts @@ -43,9 +43,10 @@ export async function link(source: string, destination: string): Promise { console.log(`Copying ${source} into ${destination}.`); shell.cp('-R', source, destination); } else { + source = p.resolve(source); console.log(`Linking ${source} into ${destination}.`); // Symlinking doesn't play nice with Jasmine's test globbing on Windows. - await fs.symlink(p.resolve(source), destination); + await fs.symlink(source, destination); } } @@ -53,8 +54,28 @@ export async function link(source: string, destination: string): Promise { export async function cleanDir(dir: string): Promise { await fs.mkdir(p.dirname(dir), {recursive: true}); try { - await fs.rmdir(dir, {recursive: true}); + await fs.rm(dir, {force: true, recursive: true}); } catch (_) { // If dir doesn't exist yet, that's fine. } } + +/// Returns whether [path1] and [path2] are symlinks that refer to the same file. +export async function sameTarget( + path1: string, + path2: string +): Promise { + const realpath1 = await tryRealpath(path1); + if (realpath1 === null) return false; + + return realpath1 === (await tryRealpath(path2)); +} + +/// Like `fs.realpath()`, but returns `null` if the path doesn't exist on disk. +async function tryRealpath(path: string): Promise { + try { + return await fs.realpath(p.resolve(path)); + } catch (_) { + return null; + } +} diff --git a/tsconfig.json b/tsconfig.json index 70d28a2d..693fadcb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,10 @@ }, "include": [ "lib/**/*.ts", - "lib/src/vendor/embedded-protocol/embedded_sass_pb.js", "tool/*.ts" ], - "exclude": ["**/*.test.ts"] + "exclude": [ + "**/*.test.ts", + "lib/src/vendor/dart-sass/**" + ] }