From 6129adfa28d5e5b6dec74e5ef5a1018f688bd38b Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 8 Sep 2023 16:07:56 -0700 Subject: [PATCH 01/10] Avoid relying on buggy import order in the legacy importer (#245) Historically, the legacy importer shim has strongly assumed that all calls to `canonicalize()` would doubled: once as a URL resolved relative to the current importer, and again as a URL passed to the importers list. However, this relies on buggy, non-spec-compliant behavior in Dart Sass: absolute URLs should never be passed to the current importer, only to the global list of importers. This change avoids relying on that behavior by instead disambiguating relative loads using a custom URL protocol prefix. All canonical URLs passed to Dart Sass now have the prefix `legacy-importer-` added to the beginning. This allows us to disambiguate relative loads, and ignore them for non-`file:` URLs, since relative loads and _only_ relative loads will have schemes beginning with `legacy-importer-`. To avoid regressing the developer experience, we then strip these prefixes from all URLs before they're passed to any developer-facing APIs or displayed to the user. --- lib/src/compile.ts | 62 +++++++++++++++++++++++++++---------- lib/src/legacy/importer.ts | 63 +++++++++++++++++++------------------- lib/src/legacy/index.ts | 55 ++++++++++++++++++--------------- lib/src/legacy/utils.ts | 59 +++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 72 deletions(-) create mode 100644 lib/src/legacy/utils.ts diff --git a/lib/src/compile.ts b/lib/src/compile.ts index c60cbf57..96d3a0a2 100644 --- a/lib/src/compile.ts +++ b/lib/src/compile.ts @@ -18,11 +18,32 @@ import {MessageTransformer} from './message-transformer'; import {PacketTransformer} from './packet-transformer'; import {SyncEmbeddedCompiler} from './sync-compiler'; import {deprotofySourceSpan} from './deprotofy-span'; -import {legacyImporterProtocol} from './legacy/importer'; +import { + removeLegacyImporter, + removeLegacyImporterFromSpan, + legacyImporterProtocol, +} from './legacy/utils'; + +/// Allow the legacy API to pass in an option signaling to the modern API that +/// it's being run in legacy mode. +/// +/// This is not intended for API users to pass in, and may be broken without +/// warning in the future. +type OptionsWithLegacy = Options & { + legacy?: boolean; +}; + +/// Allow the legacy API to pass in an option signaling to the modern API that +/// it's being run in legacy mode. +/// +/// This is not intended for API users to pass in, and may be broken without +/// warning in the future. +type StringOptionsWithLegacy = + StringOptions & {legacy?: boolean}; export function compile( path: string, - options?: Options<'sync'> + options?: OptionsWithLegacy<'sync'> ): CompileResult { const importers = new ImporterRegistry(options); return compileRequestSync( @@ -34,7 +55,7 @@ export function compile( export function compileString( source: string, - options?: StringOptions<'sync'> + options?: StringOptionsWithLegacy<'sync'> ): CompileResult { const importers = new ImporterRegistry(options); return compileRequestSync( @@ -46,7 +67,7 @@ export function compileString( export function compileAsync( path: string, - options?: Options<'async'> + options?: OptionsWithLegacy<'async'> ): Promise { const importers = new ImporterRegistry(options); return compileRequestAsync( @@ -58,7 +79,7 @@ export function compileAsync( export function compileStringAsync( source: string, - options?: StringOptions<'async'> + options?: StringOptionsWithLegacy<'async'> ): Promise { const importers = new ImporterRegistry(options); return compileRequestAsync( @@ -151,7 +172,7 @@ function newCompileRequest( async function compileRequestAsync( request: proto.InboundMessage_CompileRequest, importers: ImporterRegistry<'async'>, - options?: Options<'async'> + options?: OptionsWithLegacy<'async'> & {legacy?: boolean} ): Promise { const functions = new FunctionRegistry(options?.functions); const embeddedCompiler = new AsyncEmbeddedCompiler(); @@ -197,7 +218,7 @@ async function compileRequestAsync( function compileRequestSync( request: proto.InboundMessage_CompileRequest, importers: ImporterRegistry<'sync'>, - options?: Options<'sync'> + options?: OptionsWithLegacy<'sync'> ): CompileResult { const functions = new FunctionRegistry(options?.functions); const embeddedCompiler = new SyncEmbeddedCompiler(); @@ -272,16 +293,23 @@ function createDispatcher( /** Handles a log event according to `options`. */ function handleLogEvent( - options: Options<'sync' | 'async'> | undefined, + options: OptionsWithLegacy<'sync' | 'async'> | undefined, event: proto.OutboundMessage_LogEvent ): void { + let span = event.span ? deprotofySourceSpan(event.span) : null; + if (span && options?.legacy) span = removeLegacyImporterFromSpan(span); + let message = event.message; + if (options?.legacy) message = removeLegacyImporter(message); + let formatted = event.formatted; + if (options?.legacy) formatted = removeLegacyImporter(formatted); + if (event.type === proto.LogEventType.DEBUG) { if (options?.logger?.debug) { - options.logger.debug(event.message, { - span: deprotofySourceSpan(event.span!), + options.logger.debug(message, { + span: span!, }); } else { - console.error(event.formatted); + console.error(formatted); } } else { if (options?.logger?.warn) { @@ -289,16 +317,16 @@ function handleLogEvent( { deprecation: event.type === proto.LogEventType.DEPRECATION_WARNING, }; - - const spanProto = event.span; - if (spanProto) params.span = deprotofySourceSpan(spanProto); + if (span) params.span = span; const stack = event.stackTrace; - if (stack) params.stack = stack; + if (stack) { + params.stack = options?.legacy ? removeLegacyImporter(stack) : stack; + } - options.logger.warn(event.message, params); + options.logger.warn(message, params); } else { - console.error(event.formatted); + console.error(formatted); } } } diff --git a/lib/src/legacy/importer.ts b/lib/src/legacy/importer.ts index 149c55cf..791cfc12 100644 --- a/lib/src/legacy/importer.ts +++ b/lib/src/legacy/importer.ts @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import {strict as assert} from 'assert'; -import {pathToFileURL, URL as NodeURL} from 'url'; import * as fs from 'fs'; import * as p from 'path'; import * as util from 'util'; @@ -26,6 +25,12 @@ import { LegacyPluginThis, LegacySyncImporter, } from '../vendor/sass'; +import { + pathToLegacyFileUrl, + legacyFileUrlToPath, + legacyImporterProtocol, + legacyImporterProtocolPrefix, +} from './utils'; /** * A special URL protocol we use to signal when a stylesheet has finished @@ -36,9 +41,9 @@ import { export const endOfLoadProtocol = 'sass-embedded-legacy-load-done:'; /** - * The URL protocol to use for URLs canonicalized using `LegacyImporterWrapper`. + * The `file:` URL protocol with [legacyImporterProtocolPrefix] at the beginning. */ -export const legacyImporterProtocol = 'legacy-importer:'; +export const legacyImporterFileProtocol = 'legacy-importer-file:'; // A count of `endOfLoadProtocol` imports that have been generated. Each one // must be a different URL to ensure that the importer results aren't cached. @@ -68,11 +73,6 @@ export class LegacyImporterWrapper // `this.callbacks`, if it returned one. private lastContents: string | undefined; - // Whether we're expecting the next call to `canonicalize()` to be a relative - // load. The legacy importer API doesn't handle these loads in the same way as - // the modern API, so we always return `null` in this case. - private expectingRelativeLoad = true; - constructor( private readonly self: LegacyPluginThis, private readonly callbacks: Array>, @@ -90,14 +90,23 @@ export class LegacyImporterWrapper ): PromiseOr { if (url.startsWith(endOfLoadProtocol)) return new URL(url); - // Since there's only ever one modern importer in legacy mode, we can be - // sure that all normal loads are preceded by exactly one relative load. - if (this.expectingRelativeLoad) { - if (url.startsWith('file:')) { + if ( + url.startsWith(legacyImporterProtocolPrefix) || + url.startsWith(legacyImporterProtocol) + ) { + // A load starts with `legacyImporterProtocolPrefix` if and only if it's a + // relative load for the current importer rather than an absolute load. + // For the most part, we want to ignore these, but for `file:` URLs + // specifically we want to resolve them on the filesystem to ensure + // locality. + const urlWithoutPrefix = url.substring( + legacyImporterProtocolPrefix.length + ); + if (urlWithoutPrefix.startsWith('file:')) { let resolved: string | null = null; try { - const path = fileUrlToPathCrossPlatform(url); + const path = fileUrlToPathCrossPlatform(urlWithoutPrefix); resolved = resolvePath(path, options.fromImport); } catch (error: unknown) { if ( @@ -119,16 +128,11 @@ export class LegacyImporterWrapper if (resolved !== null) { this.prev.push({url: resolved, path: true}); - return pathToFileURL(resolved); + return pathToLegacyFileUrl(resolved); } } - this.expectingRelativeLoad = false; return null; - } else if (!url.startsWith('file:')) { - // We'll only search for another relative import when the URL isn't - // absolute. - this.expectingRelativeLoad = true; } const prev = this.prev[this.prev.length - 1]; @@ -153,14 +157,14 @@ export class LegacyImporterWrapper encodeURI((result as {file: string}).file) ); } else if (/^[A-Za-z+.-]+:/.test(url)) { - return new URL(url); + return new URL(`${legacyImporterProtocolPrefix}${url}`); } else { return new URL(legacyImporterProtocol + encodeURI(url)); } } else { if (p.isAbsolute(result.file)) { const resolved = resolvePath(result.file, options.fromImport); - return resolved ? pathToFileURL(resolved) : null; + return resolved ? pathToLegacyFileUrl(resolved) : null; } const prefixes = [...this.loadPaths, '.']; @@ -171,16 +175,16 @@ export class LegacyImporterWrapper p.join(prefix, result.file), options.fromImport ); - if (resolved !== null) return pathToFileURL(resolved); + if (resolved !== null) return pathToLegacyFileUrl(resolved); } return null; } }), result => { if (result !== null) { - const path = result.protocol === 'file:'; + const path = result.protocol === legacyImporterFileProtocol; this.prev.push({ - url: path ? fileUrlToPathCrossPlatform(result as NodeURL) : url, + url: path ? legacyFileUrlToPath(result) : url, path, }); return result; @@ -190,7 +194,7 @@ export class LegacyImporterWrapper p.join(loadPath, url), options.fromImport ); - if (resolved !== null) return pathToFileURL(resolved); + if (resolved !== null) return pathToLegacyFileUrl(resolved); } return null; } @@ -208,7 +212,7 @@ export class LegacyImporterWrapper }; } - if (canonicalUrl.protocol === 'file:') { + if (canonicalUrl.protocol === legacyImporterFileProtocol) { const syntax = canonicalUrl.pathname.endsWith('.sass') ? 'indented' : canonicalUrl.pathname.endsWith('.css') @@ -217,10 +221,7 @@ export class LegacyImporterWrapper let contents = this.lastContents ?? - fs.readFileSync( - fileUrlToPathCrossPlatform(canonicalUrl as NodeURL), - 'utf-8' - ); + fs.readFileSync(legacyFileUrlToPath(canonicalUrl), 'utf-8'); this.lastContents = undefined; if (syntax === 'scss') { contents += this.endOfLoadImport; @@ -311,7 +312,7 @@ export class LegacyImporterWrapper // The `@import` statement to inject after the contents of files to ensure // that we know when a load has completed so we can pass the correct `prev` // argument to callbacks. - private get endOfLoadImport() { + private get endOfLoadImport(): string { return `\n;@import "${endOfLoadProtocol}${endOfLoadCount++}";`; } } diff --git a/lib/src/legacy/index.ts b/lib/src/legacy/index.ts index 1631a534..8cc381e7 100644 --- a/lib/src/legacy/index.ts +++ b/lib/src/legacy/index.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; import * as p from 'path'; -import {URL, pathToFileURL} from 'url'; +import {pathToFileURL, URL} from 'url'; import {Exception} from '../exception'; import { @@ -33,11 +33,13 @@ import { StringOptions, } from '../vendor/sass'; import {wrapFunction} from './value/wrap'; +import {endOfLoadProtocol, LegacyImporterWrapper} from './importer'; import { - endOfLoadProtocol, legacyImporterProtocol, - LegacyImporterWrapper, -} from './importer'; + pathToLegacyFileUrl, + removeLegacyImporter, + removeLegacyImporterFromSpan, +} from './utils'; export function render( options: LegacyOptions<'async'>, @@ -74,8 +76,8 @@ export function renderSync(options: LegacyOptions<'sync'>): LegacyResult { } } -// Does some initial adjustments of `options` to make it easier to convert pass -// to the new API. +// Does some initial adjustments of `options` to make it easier to pass to the +// new API. function adjustOptions( options: LegacyOptions ): LegacyOptions { @@ -119,7 +121,7 @@ function isStringOptions( function convertOptions( options: LegacyOptions, sync: SyncBoolean -): Options { +): Options & {legacy: true} { if ( 'outputStyle' in options && options.outputStyle !== 'compressed' && @@ -165,6 +167,7 @@ function convertOptions( verbose: options.verbose, charset: options.charset, logger: options.logger, + legacy: true, }; } @@ -172,13 +175,15 @@ function convertOptions( function convertStringOptions( options: LegacyStringOptions, sync: SyncBoolean -): StringOptions { +): StringOptions & {legacy: true} { const modernOptions = convertOptions(options, sync); return { ...modernOptions, url: options.file - ? pathToFileURL(options.file) + ? options.importer + ? pathToLegacyFileUrl(options.file) + : pathToFileURL(options.file) : new URL(legacyImporterProtocol), importer: modernOptions.importers ? modernOptions.importers[0] : undefined, syntax: options.indentedSyntax ? 'indented' : 'scss', @@ -256,12 +261,11 @@ function newLegacyResult( sourceMap.sources = sourceMap.sources .filter(source => !source.startsWith(endOfLoadProtocol)) .map(source => { + source = removeLegacyImporter(source); if (source.startsWith('file://')) { return pathToUrlString( p.relative(sourceMapDir, fileUrlToPathCrossPlatform(source)) ); - } else if (source.startsWith(legacyImporterProtocol)) { - return source.substring(legacyImporterProtocol.length); } else if (source.startsWith('data:')) { return 'stdin'; } else { @@ -299,13 +303,14 @@ function newLegacyResult( includedFiles: result.loadedUrls .filter(url => url.protocol !== endOfLoadProtocol) .map(url => { - if (url.protocol === 'file:') { - return fileUrlToPathCrossPlatform(url as URL); - } else if (url.protocol === legacyImporterProtocol) { + if (url.protocol === legacyImporterProtocol) { return decodeURI(url.pathname); - } else { - return url.toString(); } + + const urlString = removeLegacyImporter(url.toString()); + return urlString.startsWith('file:') + ? fileUrlToPathCrossPlatform(urlString) + : urlString; }), }, }; @@ -321,25 +326,27 @@ function newLegacyException(error: Error): LegacyException { }); } + const span = error.span ? removeLegacyImporterFromSpan(error.span) : null; let file: string; - if (!error.span?.url) { + if (!span?.url) { file = 'stdin'; - } else if (error.span.url.protocol === 'file:') { + } else if (span.url.protocol === 'file:') { // We have to cast to Node's URL type here because the specified type is the // standard URL type which is slightly less featureful. `fileURLToPath()` // does work with standard URL objects in practice, but we know that we // generate Node URLs here regardless. - file = fileUrlToPathCrossPlatform(error.span.url as URL); + file = fileUrlToPathCrossPlatform(span.url as URL); } else { - file = error.span.url.toString(); + file = span.url.toString(); } + const errorString = removeLegacyImporter(error.toString()); return Object.assign(new Error(), { status: 1, - message: error.toString().replace(/^Error: /, ''), - formatted: error.toString(), - toString: () => error.toString(), - stack: error.stack, + message: errorString.replace(/^Error: /, ''), + formatted: errorString, + toString: () => errorString, + stack: error.stack ? removeLegacyImporter(error.stack) : undefined, line: isNullOrUndefined(error.span?.start.line) ? undefined : error.span!.start.line + 1, diff --git a/lib/src/legacy/utils.ts b/lib/src/legacy/utils.ts new file mode 100644 index 00000000..05ffe42b --- /dev/null +++ b/lib/src/legacy/utils.ts @@ -0,0 +1,59 @@ +// 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 {strict as assert} from 'assert'; +import {pathToFileURL} from 'url'; + +import {fileUrlToPathCrossPlatform} from '../utils'; +import {SourceSpan} from '../vendor/sass'; +import {legacyImporterFileProtocol} from './importer'; + +/** + * The URL protocol to use for URLs canonicalized using `LegacyImporterWrapper`. + */ +export const legacyImporterProtocol = 'legacy-importer:'; + +/** + * The prefix for absolute URLs canonicalized using `LegacyImporterWrapper`. + * + * This is used to distinguish imports resolved relative to URLs returned by a + * legacy importer from manually-specified absolute URLs. + */ +export const legacyImporterProtocolPrefix = 'legacy-importer-'; + +/// A regular expression that matches legacy importer protocol syntax that +/// should be removed from human-readable messages. +const removeLegacyImporterRegExp = new RegExp( + `${legacyImporterProtocol}|${legacyImporterProtocolPrefix}`, + 'g' +); + +/// Returns `string` with all instances of legacy importer syntax removed. +export function removeLegacyImporter(string: string): string { + return string.replace(removeLegacyImporterRegExp, ''); +} + +/// Returns a copy of [span] with the URL updated to remove legacy importer +/// syntax. +export function removeLegacyImporterFromSpan(span: SourceSpan): SourceSpan { + if (!span.url) return span; + return {...span, url: new URL(removeLegacyImporter(span.url.toString()))}; +} + +/// Converts [path] to a `file:` URL and adds the [legacyImporterProtocolPrefix] +/// to the beginning so we can distinguish it from manually-specified absolute +/// `file:` URLs. +export function pathToLegacyFileUrl(path: string): URL { + return new URL(`${legacyImporterProtocolPrefix}${pathToFileURL(path)}`); +} + +/// Converts a `file:` URL with [legacyImporterProtocolPrefix] to the filesystem +/// path which it represents. +export function legacyFileUrlToPath(url: URL): string { + assert.equal(url.protocol, legacyImporterFileProtocol); + const originalUrl = url + .toString() + .substring(legacyImporterProtocolPrefix.length); + return fileUrlToPathCrossPlatform(originalUrl); +} From f7509e60f8a73fb2ab653c3c5430bc7845e02f52 Mon Sep 17 00:00:00 2001 From: Sass Bot Date: Thu, 14 Sep 2023 00:56:46 +0000 Subject: [PATCH 02/10] Update Dart Sass version and release --- CHANGELOG.md | 40 +++++++++++++++++++++++++++++++++++ npm/darwin-arm64/package.json | 2 +- npm/darwin-x64/package.json | 2 +- npm/linux-arm/package.json | 2 +- npm/linux-arm64/package.json | 2 +- npm/linux-ia32/package.json | 2 +- npm/linux-x64/package.json | 2 +- npm/win32-ia32/package.json | 2 +- npm/win32-x64/package.json | 2 +- package.json | 20 +++++++++--------- 10 files changed, 58 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49739cdc..e828616a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,43 @@ +## 1.67.0 + +* All functions defined in CSS Values and Units 4 are now once again parsed as + calculation objects: `round()`, `mod()`, `rem()`, `sin()`, `cos()`, `tan()`, + `asin()`, `acos()`, `atan()`, `atan2()`, `pow()`, `sqrt()`, `hypot()`, + `log()`, `exp()`, `abs()`, and `sign()`. + + Unlike in 1.65.0, function calls are _not_ locked into being parsed as + calculations or plain Sass functions at parse-time. This means that + user-defined functions will take precedence over CSS calculations of the same + name. Although the function names `calc()` and `clamp()` are still forbidden, + users may continue to freely define functions whose names overlap with other + CSS calculations (including `abs()`, `min()`, `max()`, and `round()` whose + names overlap with global Sass functions). + +* As a consequence of the change in calculation parsing described above, + calculation functions containing interpolation are now parsed more strictly + than before. However, all interpolations that would have produced valid CSS + will continue to work, so this is not considered a breaking change. + +* Interpolations in calculation functions that aren't used in a position that + could also have a normal calculation value are now deprecated. For example, + `calc(1px #{"+ 2px"})` is deprecated, but `calc(1px + #{"2px"})` is still + allowed. This deprecation is named `calc-interp`. See [the Sass website] for + more information. + + [the Sass website]: https://sass-lang.com/d/calc-interp + +* **Potentially breaking bug fix**: The importer used to load a given file is no + longer used to load absolute URLs that appear in that file. This was + unintented behavior that contradicted the Sass specification. Absolute URLs + will now correctly be loaded only from the global importer list. This applies + to the modern JS API, the Dart API, and the embedded protocol. + +### Embedded Sass + +* Substantially improve the embedded compiler's performance when compiling many + files or files that require many importer or function call round-trips with + the embedded host. + ## 1.66.1 ### JS API diff --git a/npm/darwin-arm64/package.json b/npm/darwin-arm64/package.json index ef47479c..7ef98f92 100644 --- a/npm/darwin-arm64/package.json +++ b/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-darwin-arm64", - "version": "1.66.1", + "version": "1.67.0", "description": "The darwin-arm64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/darwin-x64/package.json b/npm/darwin-x64/package.json index 3cae9643..bb0761b6 100644 --- a/npm/darwin-x64/package.json +++ b/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-darwin-x64", - "version": "1.66.1", + "version": "1.67.0", "description": "The darwin-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-arm/package.json b/npm/linux-arm/package.json index 34901cc4..72b4db15 100644 --- a/npm/linux-arm/package.json +++ b/npm/linux-arm/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-arm", - "version": "1.66.1", + "version": "1.67.0", "description": "The linux-arm binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-arm64/package.json b/npm/linux-arm64/package.json index 2ebc4d2c..1e6ad6d3 100644 --- a/npm/linux-arm64/package.json +++ b/npm/linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-arm64", - "version": "1.66.1", + "version": "1.67.0", "description": "The linux-arm64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-ia32/package.json b/npm/linux-ia32/package.json index 31dbdf95..6bd14ee9 100644 --- a/npm/linux-ia32/package.json +++ b/npm/linux-ia32/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-ia32", - "version": "1.66.1", + "version": "1.67.0", "description": "The linux-ia32 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-x64/package.json b/npm/linux-x64/package.json index 64020198..bc2b1ada 100644 --- a/npm/linux-x64/package.json +++ b/npm/linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-x64", - "version": "1.66.1", + "version": "1.67.0", "description": "The linux-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/win32-ia32/package.json b/npm/win32-ia32/package.json index 77d85e2e..89cf8f7f 100644 --- a/npm/win32-ia32/package.json +++ b/npm/win32-ia32/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-win32-ia32", - "version": "1.66.1", + "version": "1.67.0", "description": "The win32-ia32 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/win32-x64/package.json b/npm/win32-x64/package.json index ebdbaa90..bfc2df01 100644 --- a/npm/win32-x64/package.json +++ b/npm/win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-win32-x64", - "version": "1.66.1", + "version": "1.67.0", "description": "The win32-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/package.json b/package.json index b6d9cbac..b10ab871 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "sass-embedded", - "version": "1.66.1", + "version": "1.67.0", "protocol-version": "2.1.0", - "compiler-version": "1.66.1", + "compiler-version": "1.67.0", "description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol", "repository": "sass/embedded-host-node", "author": "Google Inc.", @@ -31,14 +31,14 @@ "test": "jest" }, "optionalDependencies": { - "sass-embedded-darwin-arm64": "1.66.1", - "sass-embedded-darwin-x64": "1.66.1", - "sass-embedded-linux-arm": "1.66.1", - "sass-embedded-linux-arm64": "1.66.1", - "sass-embedded-linux-ia32": "1.66.1", - "sass-embedded-linux-x64": "1.66.1", - "sass-embedded-win32-ia32": "1.66.1", - "sass-embedded-win32-x64": "1.66.1" + "sass-embedded-darwin-arm64": "1.67.0", + "sass-embedded-darwin-x64": "1.67.0", + "sass-embedded-linux-arm": "1.67.0", + "sass-embedded-linux-arm64": "1.67.0", + "sass-embedded-linux-ia32": "1.67.0", + "sass-embedded-linux-x64": "1.67.0", + "sass-embedded-win32-ia32": "1.67.0", + "sass-embedded-win32-x64": "1.67.0" }, "dependencies": { "@bufbuild/protobuf": "^1.0.0", From ffa994740223a570cc91ff0befc5cfadf3730f59 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 18 Sep 2023 13:50:16 -0700 Subject: [PATCH 03/10] Properly include and export TypeScript types (#248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to copy the entrypoing typings file, as described in [the TypeScript docs]: > It’s important to note that the CommonJS entrypoint and the ES > module entrypoint each needs its own declaration file, even if the > contents are the same between them. Every declaration file is > interpreted either as a CommonJS module or as an ES module, based on > its file extension and the `"type"` field of the `package.json`, and > this detected module kind must match the module kind that Node will > detect for the corresponding JavaScript file for type checking to be > correct. Attempting to use a single `.d.ts` file to type both an ES > module entrypoint and a CommonJS entrypoint will cause TypeScript to > think only one of those entrypoints exists, causing compiler errors > for users of the package. [the TypeScript docs]: https://www.typescriptlang.org/docs/handbook/esm-node.html Closes #247 --- package.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b10ab871..6ae77ad2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded", - "version": "1.67.0", + "version": "1.67.1-dev", "protocol-version": "2.1.0", "compiler-version": "1.67.0", "description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol", @@ -8,7 +8,11 @@ "author": "Google Inc.", "license": "MIT", "exports": { - "import": "./dist/lib/index.mjs", + "import": { + "types": "./dist/lib/index.m.d.ts", + "default": "./dist/lib/index.mjs" + }, + "types": "./dist/lib/index.d.ts", "default": "./dist/lib/index.js" }, "main": "dist/lib/index.js", @@ -25,7 +29,7 @@ "check:gts": "gts check", "check:tsc": "tsc --noEmit", "clean": "gts clean", - "compile": "tsc && cp lib/index.mjs dist/lib/index.mjs", + "compile": "tsc && cp lib/index.mjs dist/lib/index.mjs && cp -r lib/src/vendor/sass/ dist/lib/src/vendor/sass && cp dist/lib/src/vendor/sass/index.d.ts dist/lib/src/vendor/sass/index.m.d.ts", "fix": "gts fix", "prepublishOnly": "npm run clean && ts-node ./tool/prepare-release.ts", "test": "jest" From 8e7a23aee8319d6eaf5f2cc84f7f9f04f46962a4 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 18 Sep 2023 16:00:22 -0700 Subject: [PATCH 04/10] Pass the containing URL to importers under some circumstances (#246) Closes #3247 --- lib/src/importer-registry.ts | 18 ++++++++++++++---- tool/get-language-repo.ts | 2 +- tool/utils.ts | 16 ++++++++++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/src/importer-registry.ts b/lib/src/importer-registry.ts index 7b734940..a3d825e0 100644 --- a/lib/src/importer-registry.ts +++ b/lib/src/importer-registry.ts @@ -45,7 +45,7 @@ export class ImporterRegistry { register( importer: Importer | FileImporter ): proto.InboundMessage_CompileRequest_Importer { - const response = new proto.InboundMessage_CompileRequest_Importer(); + const message = new proto.InboundMessage_CompileRequest_Importer(); if ('canonicalize' in importer) { if ('findFileUrl' in importer) { throw new Error( @@ -54,14 +54,18 @@ export class ImporterRegistry { ); } - response.importer = {case: 'importerId', value: this.id}; + message.importer = {case: 'importerId', value: this.id}; + message.nonCanonicalScheme = + typeof importer.nonCanonicalScheme === 'string' + ? [importer.nonCanonicalScheme] + : importer.nonCanonicalScheme ?? []; this.importersById.set(this.id, importer); } else { - response.importer = {case: 'fileImporterId', value: this.id}; + message.importer = {case: 'fileImporterId', value: this.id}; this.fileImportersById.set(this.id, importer); } this.id += 1; - return response; + return message; } /** Handles a canonicalization request. */ @@ -78,6 +82,9 @@ export class ImporterRegistry { return thenOr( importer.canonicalize(request.url, { fromImport: request.fromImport, + containingUrl: request.containingUrl + ? new URL(request.containingUrl) + : null, }), url => new proto.InboundMessage_CanonicalizeResponse({ @@ -157,6 +164,9 @@ export class ImporterRegistry { return thenOr( importer.findFileUrl(request.url, { fromImport: request.fromImport, + containingUrl: request.containingUrl + ? new URL(request.containingUrl) + : null, }), url => { if (!url) return new proto.InboundMessage_FileImportResponse(); diff --git a/tool/get-language-repo.ts b/tool/get-language-repo.ts index 83a264b8..cd8da97c 100644 --- a/tool/get-language-repo.ts +++ b/tool/get-language-repo.ts @@ -32,7 +32,7 @@ export async function getLanguageRepo( // 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'); + if (process.platform === 'win32') shell.rm('build/sass/spec/README.md'); await utils.link('build/sass/js-api-doc', p.join(outPath, 'sass')); diff --git a/tool/utils.ts b/tool/utils.ts index 3e4a343c..9dc1af5a 100644 --- a/tool/utils.ts +++ b/tool/utils.ts @@ -2,7 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import {promises as fs, existsSync} from 'fs'; +import {promises as fs, existsSync, lstatSync} from 'fs'; import * as p from 'path'; import * as shell from 'shelljs'; @@ -17,13 +17,21 @@ export function fetchRepo(options: { outPath: string; ref: string; }): void { - if (!existsSync(p.join(options.outPath, options.repo))) { + const path = p.join(options.outPath, options.repo); + if (lstatSync(path).isSymbolicLink() && existsSync(p.join(path, '.git'))) { + throw ( + `${path} is a symlink to a git repo, not overwriting.\n` + + `Run "rm ${path}" and try again.` + ); + } + + if (!existsSync(path)) { console.log(`Cloning ${options.repo} into ${options.outPath}.`); shell.exec( `git clone \ --depth=1 \ https://github.com/sass/${options.repo} \ - ${p.join(options.outPath, options.repo)}`, + ${path}`, {silent: true} ); } @@ -35,7 +43,7 @@ export function fetchRepo(options: { `git fetch --depth=1 origin ${options.ref} && git reset --hard FETCH_HEAD`, { silent: true, - cwd: p.join(options.outPath, options.repo), + cwd: path, } ); } From 37acf72ec0d5f39203c92dfcd9af2e8aee02a852 Mon Sep 17 00:00:00 2001 From: Sass Bot Date: Thu, 21 Sep 2023 01:14:40 +0000 Subject: [PATCH 05/10] Update Dart Sass version and release --- CHANGELOG.md | 56 +++++++++++++++++++++++++++-------- npm/darwin-arm64/package.json | 2 +- npm/darwin-x64/package.json | 2 +- npm/linux-arm/package.json | 2 +- npm/linux-arm64/package.json | 2 +- npm/linux-ia32/package.json | 2 +- npm/linux-x64/package.json | 2 +- npm/win32-ia32/package.json | 2 +- npm/win32-x64/package.json | 2 +- package.json | 20 ++++++------- 10 files changed, 62 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e828616a..e56a37ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +## 1.68.0 + +* Fix the source spans associated with the `abs-percent` deprecation. + +### JS API + +* Non-filesystem importers can now set the `nonCanonicalScheme` field, which + declares that one or more URL schemes (without `:`) will never be used for + URLs returned by the `canonicalize()` method. + +* Add a `containingUrl` field to the `canonicalize()` and `findFileUrl()` + methods of importers, which is set to the canonical URL of the stylesheet that + contains the current load. For filesystem importers, this is always set; for + other importers, it's set only if the current load has no URL scheme, or if + its URL scheme is declared as non-canonical by the importer. + +### Dart API + +* Add `AsyncImporter.isNonCanonicalScheme`, which importers (async or sync) can + use to indicate that a certain URL scheme will never be used for URLs returned + by the `canonicalize()` method. + +* Add `AsyncImporter.containingUrl`, which is set during calls to the + `canonicalize()` method to the canonical URL of the stylesheet that contains + the current load. This is set only if the current load has no URL scheme, or + if its URL scheme is declared as non-canonical by the importer. + +### Embedded Sass + +* The `CalculationValue.interpolation` field is deprecated and will be removed + in a future version. It will no longer be set by the compiler, and if the host + sets it it will be treated as equivalent to `CalculationValue.string` except + that `"("` and `")"` will be added to the beginning and end of the string + values. + +* Properly include TypeScript types in the `sass-embedded` package. + ## 1.67.0 * All functions defined in CSS Values and Units 4 are now once again parsed as @@ -13,18 +50,13 @@ CSS calculations (including `abs()`, `min()`, `max()`, and `round()` whose names overlap with global Sass functions). -* As a consequence of the change in calculation parsing described above, - calculation functions containing interpolation are now parsed more strictly - than before. However, all interpolations that would have produced valid CSS - will continue to work, so this is not considered a breaking change. - -* Interpolations in calculation functions that aren't used in a position that - could also have a normal calculation value are now deprecated. For example, - `calc(1px #{"+ 2px"})` is deprecated, but `calc(1px + #{"2px"})` is still - allowed. This deprecation is named `calc-interp`. See [the Sass website] for - more information. - - [the Sass website]: https://sass-lang.com/d/calc-interp +* **Breaking change**: As a consequence of the change in calculation parsing + described above, calculation functions containing interpolation are now parsed + more strictly than before. However, _almost_ all interpolations that would + have produced valid CSS will continue to work. The only exception is + `#{$variable}%` which is not valid in Sass and is no longer valid in + calculations. Instead of this, either use `$variable` directly and ensure it + already has the `%` unit, or write `($variable * 1%)`. * **Potentially breaking bug fix**: The importer used to load a given file is no longer used to load absolute URLs that appear in that file. This was diff --git a/npm/darwin-arm64/package.json b/npm/darwin-arm64/package.json index 7ef98f92..2bd3811a 100644 --- a/npm/darwin-arm64/package.json +++ b/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-darwin-arm64", - "version": "1.67.0", + "version": "1.68.0", "description": "The darwin-arm64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/darwin-x64/package.json b/npm/darwin-x64/package.json index bb0761b6..08d5ca1c 100644 --- a/npm/darwin-x64/package.json +++ b/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-darwin-x64", - "version": "1.67.0", + "version": "1.68.0", "description": "The darwin-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-arm/package.json b/npm/linux-arm/package.json index 72b4db15..cdd33f03 100644 --- a/npm/linux-arm/package.json +++ b/npm/linux-arm/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-arm", - "version": "1.67.0", + "version": "1.68.0", "description": "The linux-arm binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-arm64/package.json b/npm/linux-arm64/package.json index 1e6ad6d3..4719d518 100644 --- a/npm/linux-arm64/package.json +++ b/npm/linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-arm64", - "version": "1.67.0", + "version": "1.68.0", "description": "The linux-arm64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-ia32/package.json b/npm/linux-ia32/package.json index 6bd14ee9..8d48cf15 100644 --- a/npm/linux-ia32/package.json +++ b/npm/linux-ia32/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-ia32", - "version": "1.67.0", + "version": "1.68.0", "description": "The linux-ia32 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/linux-x64/package.json b/npm/linux-x64/package.json index bc2b1ada..4bc67f55 100644 --- a/npm/linux-x64/package.json +++ b/npm/linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-linux-x64", - "version": "1.67.0", + "version": "1.68.0", "description": "The linux-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/win32-ia32/package.json b/npm/win32-ia32/package.json index 89cf8f7f..4137c236 100644 --- a/npm/win32-ia32/package.json +++ b/npm/win32-ia32/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-win32-ia32", - "version": "1.67.0", + "version": "1.68.0", "description": "The win32-ia32 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/npm/win32-x64/package.json b/npm/win32-x64/package.json index bfc2df01..062fbdc1 100644 --- a/npm/win32-x64/package.json +++ b/npm/win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "sass-embedded-win32-x64", - "version": "1.67.0", + "version": "1.68.0", "description": "The win32-x64 binary for sass-embedded", "repository": "sass/embedded-host-node", "author": "Google Inc.", diff --git a/package.json b/package.json index 6ae77ad2..587af699 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "sass-embedded", - "version": "1.67.1-dev", + "version": "1.68.0", "protocol-version": "2.1.0", - "compiler-version": "1.67.0", + "compiler-version": "1.68.0", "description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol", "repository": "sass/embedded-host-node", "author": "Google Inc.", @@ -35,14 +35,14 @@ "test": "jest" }, "optionalDependencies": { - "sass-embedded-darwin-arm64": "1.67.0", - "sass-embedded-darwin-x64": "1.67.0", - "sass-embedded-linux-arm": "1.67.0", - "sass-embedded-linux-arm64": "1.67.0", - "sass-embedded-linux-ia32": "1.67.0", - "sass-embedded-linux-x64": "1.67.0", - "sass-embedded-win32-ia32": "1.67.0", - "sass-embedded-win32-x64": "1.67.0" + "sass-embedded-darwin-arm64": "1.68.0", + "sass-embedded-darwin-x64": "1.68.0", + "sass-embedded-linux-arm": "1.68.0", + "sass-embedded-linux-arm64": "1.68.0", + "sass-embedded-linux-ia32": "1.68.0", + "sass-embedded-linux-x64": "1.68.0", + "sass-embedded-win32-ia32": "1.68.0", + "sass-embedded-win32-x64": "1.68.0" }, "dependencies": { "@bufbuild/protobuf": "^1.0.0", From 1784d46bec6ddf05bdfc4695b4aef160f33b0b52 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 28 Sep 2023 10:59:56 -0700 Subject: [PATCH 06/10] Update the embedded protocol versino (#251) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 587af699..f71b4129 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sass-embedded", "version": "1.68.0", - "protocol-version": "2.1.0", + "protocol-version": "2.2.0", "compiler-version": "1.68.0", "description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol", "repository": "sass/embedded-host-node", From 6849715fcf9692be874cfb61bee33f730451136d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 28 Sep 2023 13:02:40 -0700 Subject: [PATCH 07/10] Forbid LLM contributions (#250) --- CONTRIBUTING.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7c61af0..bcdbc8c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,15 @@ We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. +* [Contributor License Agreement](#contributor-license-agreement) +* [Code Reviews](#code-reviews) +* [Large Language Models](#large-language-models) +* [Release Process](#release-process) +* [Keeping in Sync With Other Packages](#keeping-in-sync-with-other-packages) + * [Local Development](#local-development) + * [Continuous Integration](#continuous-integration) + * [Release](#release) + ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License @@ -15,13 +24,24 @@ You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. -## Code reviews +## Code Reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. +## Large Language Models + +Do not submit any code or prose written or modified by large language models or +"artificial intelligence" such as GitHub Copilot or ChatGPT to this project. +These tools produce code that looks plausible, which means that not only is it +likely to contain bugs those bugs are likely to be difficult to notice on +review. In addition, because these models were trained indiscriminately and +non-consensually on open-source code with a variety of licenses, it's not +obvious that we have the moral or legal right to redistribute code they +generate. + ## Release process Because this package's version remains in lockstep with the current version of @@ -36,7 +56,7 @@ such, manual commits should never: * Update the `package.json`'s `"compiler-version"` field to a non-`-dev` number. Changing it from non-`-dev` to dev when using a new feature is fine. -# Keeping in Sync With Other Packages +## Keeping in Sync With Other Packages The embedded host depends on several different components which come from different repositories: @@ -51,7 +71,7 @@ different repositories: These dependencies are made available in different ways depending on context. -## Local Development +### Local Development When developing locally, you can download all of these dependencies by running `npm run init`. This provides the following options for `compiler` (for the @@ -77,14 +97,14 @@ By default: * This uses the Dart Sass version from the latest revision on GitHub, unless the `--compiler-path` was passed in which case it uses that version of Dart Sass. -## Continuous Integration +### 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 Dart Sass, the embedded protocol, or the JS API, this will check out that version and run tests against it instead. -## Release +### Release 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 From 79ab0914f93fd15409f4ae778b8d4459512dd980 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:36:51 -0700 Subject: [PATCH 08/10] Bump minipass from 7.0.3 to 7.0.4 (#254) Bumps [minipass](https://github.com/isaacs/minipass) from 7.0.3 to 7.0.4. - [Changelog](https://github.com/isaacs/minipass/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/minipass/compare/v7.0.3...v7.0.4) --- updated-dependencies: - dependency-name: minipass dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f71b4129..1e88355f 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "extract-zip": "^2.0.1", "gts": "^5.0.0", "jest": "^29.4.1", - "minipass": "7.0.3", + "minipass": "7.0.4", "npm-run-all": "^4.1.5", "shelljs": "^0.8.4", "source-map-js": "^1.0.2", From c9dee9b22e4e5619e86ee78c44c41fe83b5add55 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 5 Oct 2023 14:10:47 -0700 Subject: [PATCH 09/10] Fix new eslint issues (#255) --- lib/src/sync-process/index.ts | 2 +- package.json | 2 +- tool/init.ts | 2 +- tool/prepare-optional-release.ts | 2 +- tool/prepare-release.ts | 2 +- tool/utils.ts | 2 +- tsconfig.build.json | 7 +++++++ tsconfig.json | 9 ++++----- 8 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 tsconfig.build.json diff --git a/lib/src/sync-process/index.ts b/lib/src/sync-process/index.ts index 286ad346..426dbb1c 100644 --- a/lib/src/sync-process/index.ts +++ b/lib/src/sync-process/index.ts @@ -141,7 +141,7 @@ export class SyncProcess { /** Closes down the worker thread and the stdin stream. */ private close(): void { this.port.close(); - this.worker.terminate(); + void this.worker.terminate(); this.stdin.destroy(); } } diff --git a/package.json b/package.json index 1e88355f..32bdafa6 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "check:gts": "gts check", "check:tsc": "tsc --noEmit", "clean": "gts clean", - "compile": "tsc && cp lib/index.mjs dist/lib/index.mjs && cp -r lib/src/vendor/sass/ dist/lib/src/vendor/sass && cp dist/lib/src/vendor/sass/index.d.ts dist/lib/src/vendor/sass/index.m.d.ts", + "compile": "tsc -p tsconfig.build.json && cp lib/index.mjs dist/lib/index.mjs && cp -r lib/src/vendor/sass/ dist/lib/src/vendor/sass && cp dist/lib/src/vendor/sass/index.d.ts dist/lib/src/vendor/sass/index.m.d.ts", "fix": "gts fix", "prepublishOnly": "npm run clean && ts-node ./tool/prepare-release.ts", "test": "jest" diff --git a/tool/init.ts b/tool/init.ts index cb50c12b..a178675f 100644 --- a/tool/init.ts +++ b/tool/init.ts @@ -36,7 +36,7 @@ const argv = yargs(process.argv.slice(2)) }) .parseSync(); -(async () => { +void (async () => { try { const outPath = 'lib/src/vendor'; diff --git a/tool/prepare-optional-release.ts b/tool/prepare-optional-release.ts index 9ad7cb68..36a42ba7 100644 --- a/tool/prepare-optional-release.ts +++ b/tool/prepare-optional-release.ts @@ -104,7 +104,7 @@ async function downloadRelease(options: { await fs.unlink(zippedAssetPath); } -(async () => { +void (async () => { try { const version = pkg['compiler-version'] as string; if (version.endsWith('-dev')) { diff --git a/tool/prepare-release.ts b/tool/prepare-release.ts index 8d4c08b4..30b49618 100644 --- a/tool/prepare-release.ts +++ b/tool/prepare-release.ts @@ -8,7 +8,7 @@ import * as shell from 'shelljs'; import * as pkg from '../package.json'; import {getLanguageRepo} from './get-language-repo'; -(async () => { +void (async () => { try { await sanityCheckBeforeRelease(); diff --git a/tool/utils.ts b/tool/utils.ts index 9dc1af5a..e0cc581e 100644 --- a/tool/utils.ts +++ b/tool/utils.ts @@ -18,7 +18,7 @@ export function fetchRepo(options: { ref: string; }): void { const path = p.join(options.outPath, options.repo); - if (lstatSync(path).isSymbolicLink() && existsSync(p.join(path, '.git'))) { + if (existsSync(p.join(path, '.git')) && lstatSync(path).isSymbolicLink()) { throw ( `${path} is a symlink to a git repo, not overwriting.\n` + `Run "rm ${path}" and try again.` diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..0673a92b --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "lib/src/vendor/dart-sass/**", + "**/*.test.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 693fadcb..c839cc1e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,11 +11,10 @@ "lib": ["DOM"] }, "include": [ + "*.ts", "lib/**/*.ts", - "tool/*.ts" + "tool/**/*.ts", + "test/**/*.ts" ], - "exclude": [ - "**/*.test.ts", - "lib/src/vendor/dart-sass/**" - ] + "exclude": ["lib/src/vendor/dart-sass/**"] } From 9e354f020c0060880967ba2aa757b167f299a553 Mon Sep 17 00:00:00 2001 From: Connor Skees <39542938+connorskees@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:50:58 -0700 Subject: [PATCH 10/10] Add first class mixins (#253) Co-authored-by: Natalie Weizenbaum --- CONTRIBUTING.md | 9 +++++--- lib/index.mjs | 5 +++++ lib/index.ts | 1 + lib/src/protofier.ts | 8 +++++++ lib/src/value/index.ts | 12 ++++++++++ lib/src/value/mixin.ts | 41 +++++++++++++++++++++++++++++++++++ package.json | 2 +- tool/get-embedded-compiler.ts | 2 +- 8 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 lib/src/value/mixin.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcdbc8c7..cc5fbeb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,9 +74,9 @@ These dependencies are made available in different ways depending on context. ### Local Development When developing locally, you can download all of these dependencies by running -`npm run init`. This provides the following options for `compiler` (for the -embedded compiler), `protocol` (for the embedded protocol), and `api` (for the -JS API): +`npm install` and then `npm run init`. This provides the following options for +`compiler` (for the embedded compiler), `protocol` (for the embedded protocol), +and `api` (for the JS API): * `---path`: The local filesystem path of the package to use. This is useful when doing local development on both the host and its dependencies at @@ -85,6 +85,9 @@ JS API): * `---ref`: A Git reference for the GitHub repository of the package to clone. +If developing locally, you will need to specify both the compiler and language. +For example: `npm run init -- --compiler-path=dart-sass --language-path=language`. + By default: * This uses the version of the embedded protocol and compiler specified by diff --git a/lib/index.mjs b/lib/index.mjs index 374e98e9..ff94ed4e 100644 --- a/lib/index.mjs +++ b/lib/index.mjs @@ -13,6 +13,7 @@ export const SassBoolean = sass.SassBoolean; export const SassCalculation = sass.SassCalculation export const SassColor = sass.SassColor; export const SassFunction = sass.SassFunction; +export const SassMixin = sass.SassMixin; export const SassList = sass.SassList; export const SassMap = sass.SassMap; export const SassNumber = sass.SassNumber; @@ -95,6 +96,10 @@ export default { defaultExportDeprecation(); return sass.SassFunction; }, + get SassMixin() { + defaultExportDeprecation(); + return sass.SassMixin; + }, get SassList() { defaultExportDeprecation(); return sass.SassList; diff --git a/lib/index.ts b/lib/index.ts index c79ffec2..69005262 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -12,6 +12,7 @@ export {sassFalse, sassTrue} from './src/value/boolean'; export {SassColor} from './src/value/color'; export {SassFunction} from './src/value/function'; export {SassMap} from './src/value/map'; +export {SassMixin} from './src/value/mixin'; export {SassNumber} from './src/value/number'; export {SassString} from './src/value/string'; export {Value} from './src/value'; diff --git a/lib/src/protofier.ts b/lib/src/protofier.ts index 0092cc11..e2d7f951 100644 --- a/lib/src/protofier.ts +++ b/lib/src/protofier.ts @@ -24,6 +24,7 @@ import { CalculationOperation, CalculationOperator, } from './value/calculations'; +import {SassMixin} from './value/mixin'; /** * A class that converts [Value] objects into protobufs. @@ -119,6 +120,10 @@ export class Protofier { fn.signature = value.signature!; result.value = {case: 'hostFunction', value: fn}; } + } else if (value instanceof SassMixin) { + const mixin = new proto.Value_CompilerMixin(); + mixin.id = value.id; + result.value = {case: 'compilerMixin', value: mixin}; } else if (value instanceof SassCalculation) { result.value = { case: 'calculation', @@ -322,6 +327,9 @@ export class Protofier { 'The compiler may not send Value.host_function.' ); + case 'compilerMixin': + return new SassMixin(value.value.value.id); + case 'calculation': return this.deprotofyCalculation(value.value.value); diff --git a/lib/src/value/index.ts b/lib/src/value/index.ts index d7c95f8e..6e5129fe 100644 --- a/lib/src/value/index.ts +++ b/lib/src/value/index.ts @@ -12,6 +12,7 @@ import {SassNumber} from './number'; import {SassString} from './string'; import {valueError} from '../utils'; import {SassCalculation} from './calculations'; +import {SassMixin} from './mixin'; /** * A SassScript value. @@ -139,6 +140,17 @@ export abstract class Value implements ValueObject { // TODO(awjin): Narrow the return type to SassFunction. } + /** + * Casts `this` to `SassMixin`; throws if `this` isn't a mixin + * reference. + * + * If `this` came from a function argument, `name` is the argument name + * (without the `$`) and is used for error reporting. + */ + assertMixin(name?: string): SassMixin { + throw valueError(`${this} is not a mixin reference`, name); + } + /** * Casts `this` to `SassMap`; throws if `this` isn't a map. * diff --git a/lib/src/value/mixin.ts b/lib/src/value/mixin.ts new file mode 100644 index 00000000..1d8ecd86 --- /dev/null +++ b/lib/src/value/mixin.ts @@ -0,0 +1,41 @@ +// Copyright 2021 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 {hash} from 'immutable'; + +import {Value} from './index'; + +/** A first-class SassScript mixin. */ +export class SassMixin extends Value { + /** + * This is the unique ID that the compiler uses to determine which mixin it + * refers to. + * + * This is marked as public so that the protofier can access it, but it's not + * part of the package's public API and should not be accessed by user code. + * It may be renamed or removed without warning in the future. + */ + readonly id: number; + + constructor(id: number) { + super(); + this.id = id; + } + + equals(other: Value): boolean { + return other instanceof SassMixin && other.id === this.id; + } + + hashCode(): number { + return hash(this.id); + } + + toString(): string { + return ``; + } + + assertMixin(): SassMixin { + return this; + } +} diff --git a/package.json b/package.json index 32bdafa6..567126ac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sass-embedded", "version": "1.68.0", - "protocol-version": "2.2.0", + "protocol-version": "2.3.0", "compiler-version": "1.68.0", "description": "Node.js library that communicates with Embedded Dart Sass using the Embedded Sass protocol", "repository": "sass/embedded-host-node", diff --git a/tool/get-embedded-compiler.ts b/tool/get-embedded-compiler.ts index 2b702a2a..cb8b7245 100644 --- a/tool/get-embedded-compiler.ts +++ b/tool/get-embedded-compiler.ts @@ -8,7 +8,7 @@ import * as shell from 'shelljs'; import * as utils from './utils'; /** - * Downlaods and builds the Embedded Dart Sass compiler. + * Downloads and builds the Embedded Dart Sass compiler. * * 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.