From d5e63ffee623b5bfbe04d992c289b60c8fa1bf2e Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Fri, 13 Sep 2024 16:23:11 -0700 Subject: [PATCH 1/4] Emit deprecation warnings for legacy JS API --- lib/src/deprecations.ts | 77 +++++++++++++++++++++++++++++++++++++++-- lib/src/legacy/index.ts | 11 ++++++ lib/src/value/color.ts | 33 +++++++++--------- 3 files changed, 102 insertions(+), 19 deletions(-) diff --git a/lib/src/deprecations.ts b/lib/src/deprecations.ts index 26341d82..05ce3c37 100644 --- a/lib/src/deprecations.ts +++ b/lib/src/deprecations.ts @@ -2,7 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import {DeprecationOrId} from './vendor/sass'; +import {Deprecation, DeprecationOrId} from './vendor/sass'; import {Version} from './version'; export {deprecations} from './vendor/deprecations'; @@ -15,12 +15,83 @@ export {Deprecation, DeprecationOrId, DeprecationStatus} from './vendor/sass'; export function getDeprecationIds( arr: (DeprecationOrId | Version)[] ): string[] { - return arr.flatMap(item => { + return arr.map(item => { if (item instanceof Version) { - return arr.map(item => item.toString()); + return item.toString(); } else if (typeof item === 'string') { return item; } return item.id; }); } + +/** + * Shorthand for the subset of options related to deprecations. + */ +export type DeprecationOptions = { + fatalDeprecations?: (DeprecationOrId | Version)[]; + futureDeprecations?: DeprecationOrId[]; + silenceDeprecations?: DeprecationOrId[]; +}; + +/** + * Handles a host-side deprecation warning, either emitting a warning, throwing + * and error, or doing nothing depending on the deprecation options used. + */ +export function warnForHostSideDeprecation( + message: string, + deprecation: Deprecation, + options?: DeprecationOptions +): void { + if ( + deprecation.status === 'future' && + !getDeprecationIds(options?.futureDeprecations ?? []).includes( + deprecation.id + ) + ) { + return; + } + const fullMessage = `Deprecation [${deprecation.id}]: ${message}`; + if (isFatal(deprecation, options)) { + throw Error(fullMessage); + } + if ( + !getDeprecationIds(options?.silenceDeprecations ?? []).includes( + deprecation.id + ) + ) { + console.warn(fullMessage); + } +} + +/** + * Checks whether the given deprecation is included in the given list of + * fatal deprecations. + */ +function isFatal( + deprecation: Deprecation, + options?: DeprecationOptions +): boolean { + const versionNumber = + deprecation.deprecatedIn === null + ? null + : deprecation.deprecatedIn.major * 1000000 + + deprecation.deprecatedIn.minor * 1000 + + deprecation.deprecatedIn.patch; + for (const fatal of options?.fatalDeprecations ?? []) { + if (fatal instanceof Version) { + if (versionNumber === null) continue; + if ( + versionNumber <= + fatal.major * 1000000 + fatal.minor * 1000 + fatal.patch + ) { + return true; + } + } else if (typeof fatal === 'string') { + if (fatal === deprecation.id) return true; + } else { + if (fatal.id === deprecation.id) return true; + } + } + return false; +} diff --git a/lib/src/legacy/index.ts b/lib/src/legacy/index.ts index 65e6c937..67f6e581 100644 --- a/lib/src/legacy/index.ts +++ b/lib/src/legacy/index.ts @@ -14,6 +14,7 @@ import { compileString, compileStringAsync, } from '../compile'; +import {deprecations, warnForHostSideDeprecation} from '../deprecations'; import { SyncBoolean, fileUrlToPathCrossPlatform, @@ -50,6 +51,11 @@ export function render( options = adjustOptions(options); const start = Date.now(); + warnForHostSideDeprecation( + 'The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.', + deprecations['legacy-js-api'], + options + ); const compileSass = isStringOptions(options) ? compileStringAsync(options.data, convertStringOptions(options, false)) : compileAsync(options.file, convertOptions(options, false)); @@ -68,6 +74,11 @@ export function renderSync(options: LegacyOptions<'sync'>): LegacyResult { const start = Date.now(); try { options = adjustOptions(options); + warnForHostSideDeprecation( + 'The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.', + deprecations['legacy-js-api'], + options + ); const result = isStringOptions(options) ? compileString(options.data, convertStringOptions(options, true)) : compile(options.file, convertOptions(options, true)); diff --git a/lib/src/value/color.ts b/lib/src/value/color.ts index 5cf260d4..9cb6344a 100644 --- a/lib/src/value/color.ts +++ b/lib/src/value/color.ts @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import {Value} from './index'; +import {deprecations, warnForHostSideDeprecation} from '../deprecations'; import {valueError} from '../utils'; import { fuzzyAssertInRange, @@ -304,11 +305,11 @@ function checkChangeDeprecations( /** Warn users about legacy color channel getters. */ function emitColor4ApiGetterDeprecation(name: string): void { - console.warn( - 'Deprecation [color-4-api]: ' + - `\`${name}\` is deprecated, use \`channel\` instead.` + + warnForHostSideDeprecation( + `\`${name}\` is deprecated, use \`channel\` instead.` + '\n' + - 'More info: https://sass-lang.com/d/color-4-api' + 'More info: https://sass-lang.com/d/color-4-api', + deprecations['color-4-api'] ); } @@ -317,32 +318,32 @@ function emitColor4ApiGetterDeprecation(name: string): void { * explicitly setting `space`. */ function emitColor4ApiChangeSpaceDeprecation(): void { - console.warn( - 'Deprecation [color-4-api]: ' + - "Changing a channel not in this color's space without explicitly " + + warnForHostSideDeprecation( + "Changing a channel not in this color's space without explicitly " + 'specifying the `space` option is deprecated.' + '\n' + - 'More info: https://sass-lang.com/d/color-4-api' + 'More info: https://sass-lang.com/d/color-4-api', + deprecations['color-4-api'] ); } /** Warn users about `null` channel values without setting `space`. */ function emitColor4ApiChangeNullDeprecation(channel: string): void { - console.warn( - 'Deprecation [color-4-api]: ' + - `Passing \`${channel}: null\` without setting \`space\` is deprecated.` + + warnForHostSideDeprecation( + `Passing \`${channel}: null\` without setting \`space\` is deprecated.` + '\n' + - 'More info: https://sass-lang.com/d/color-4-api' + 'More info: https://sass-lang.com/d/color-4-api', + deprecations['color-4-api'] ); } /** Warn users about null-alpha deprecation. */ function emitNullAlphaDeprecation(): void { - console.warn( - 'Deprecation [null-alpha]: ' + - 'Passing `alpha: null` without setting `space` is deprecated.' + + warnForHostSideDeprecation( + 'Passing `alpha: null` without setting `space` is deprecated.' + '\n' + - 'More info: https://sass-lang.com/d/null-alpha' + 'More info: https://sass-lang.com/d/null-alpha', + deprecations['null-alpha'] ); } From eeb1a7807cbee6d60f46bddede1208acfaa82d2d Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Mon, 16 Sep 2024 15:10:12 -0700 Subject: [PATCH 2/4] Support flags for color-4-api deprecation --- lib/src/compiler/async.ts | 69 +++++++++++++++++--------------- lib/src/compiler/sync.ts | 81 ++++++++++++++++++++----------------- lib/src/deprecations.ts | 84 +++++++++++++++++++++++++++++++-------- 3 files changed, 149 insertions(+), 85 deletions(-) diff --git a/lib/src/compiler/async.ts b/lib/src/compiler/async.ts index 417b82a9..ad577db2 100644 --- a/lib/src/compiler/async.ts +++ b/lib/src/compiler/async.ts @@ -17,6 +17,7 @@ import { newCompileStringRequest, } from './utils'; import {compilerCommand} from '../compiler-path'; +import {activeDeprecationOptions} from '../deprecations'; import {FunctionRegistry} from '../function-registry'; import {ImporterRegistry} from '../importer-registry'; import {MessageTransformer} from '../message-transformer'; @@ -102,38 +103,44 @@ export class AsyncCompiler { importers: ImporterRegistry<'async'>, options?: OptionsWithLegacy<'async'> & {legacy?: boolean} ): Promise { - const functions = new FunctionRegistry(options?.functions); - - const dispatcher = createDispatcher<'async'>( - this.compilationId++, - this.messageTransformer, - { - handleImportRequest: request => importers.import(request), - handleFileImportRequest: request => importers.fileImport(request), - handleCanonicalizeRequest: request => importers.canonicalize(request), - handleFunctionCallRequest: request => functions.call(request), - } - ); - dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); - - const compilation = new Promise( - (resolve, reject) => - dispatcher.sendCompileRequest(request, (err, response) => { - this.compilations.delete(compilation); - // Reset the compilation ID when the compiler goes idle (no active - // compilations) to avoid overflowing it. - // https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794 - if (this.compilations.size === 0) this.compilationId = 1; - if (err) { - reject(err); - } else { - resolve(response!); - } - }) - ); - this.compilations.add(compilation); + const optionsKey = Symbol(); + activeDeprecationOptions.set(optionsKey, options ?? {}); + try { + const functions = new FunctionRegistry(options?.functions); + + const dispatcher = createDispatcher<'async'>( + this.compilationId++, + this.messageTransformer, + { + handleImportRequest: request => importers.import(request), + handleFileImportRequest: request => importers.fileImport(request), + handleCanonicalizeRequest: request => importers.canonicalize(request), + handleFunctionCallRequest: request => functions.call(request), + } + ); + dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); + + const compilation = new Promise( + (resolve, reject) => + dispatcher.sendCompileRequest(request, (err, response) => { + this.compilations.delete(compilation); + // Reset the compilation ID when the compiler goes idle (no active + // compilations) to avoid overflowing it. + // https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794 + if (this.compilations.size === 0) this.compilationId = 1; + if (err) { + reject(err); + } else { + resolve(response!); + } + }) + ); + this.compilations.add(compilation); - return handleCompileResponse(await compilation); + return handleCompileResponse(await compilation); + } finally { + activeDeprecationOptions.delete(optionsKey); + } } /** Initialize resources shared across compilations. */ diff --git a/lib/src/compiler/sync.ts b/lib/src/compiler/sync.ts index 33838e0e..936ca903 100644 --- a/lib/src/compiler/sync.ts +++ b/lib/src/compiler/sync.ts @@ -14,6 +14,7 @@ import { newCompileStringRequest, } from './utils'; import {compilerCommand} from '../compiler-path'; +import {activeDeprecationOptions} from '../deprecations'; import {Dispatcher} from '../dispatcher'; import {FunctionRegistry} from '../function-registry'; import {ImporterRegistry} from '../importer-registry'; @@ -108,44 +109,50 @@ export class Compiler { importers: ImporterRegistry<'sync'>, options?: OptionsWithLegacy<'sync'> ): CompileResult { - const functions = new FunctionRegistry(options?.functions); - - const dispatcher = createDispatcher<'sync'>( - this.compilationId++, - this.messageTransformer, - { - handleImportRequest: request => importers.import(request), - handleFileImportRequest: request => importers.fileImport(request), - handleCanonicalizeRequest: request => importers.canonicalize(request), - handleFunctionCallRequest: request => functions.call(request), - } - ); - this.dispatchers.add(dispatcher); - - dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); - - let error: unknown; - let response: proto.OutboundMessage_CompileResponse | undefined; - dispatcher.sendCompileRequest(request, (error_, response_) => { - this.dispatchers.delete(dispatcher); - // Reset the compilation ID when the compiler goes idle (no active - // dispatchers) to avoid overflowing it. - // https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794 - if (this.dispatchers.size === 0) this.compilationId = 1; - if (error_) { - error = error_; - } else { - response = response_; - } - }); - - for (;;) { - if (!this.yield()) { - throw utils.compilerError('Embedded compiler exited unexpectedly.'); + const optionsKey = Symbol(); + activeDeprecationOptions.set(optionsKey, options ?? {}); + try { + const functions = new FunctionRegistry(options?.functions); + + const dispatcher = createDispatcher<'sync'>( + this.compilationId++, + this.messageTransformer, + { + handleImportRequest: request => importers.import(request), + handleFileImportRequest: request => importers.fileImport(request), + handleCanonicalizeRequest: request => importers.canonicalize(request), + handleFunctionCallRequest: request => functions.call(request), + } + ); + this.dispatchers.add(dispatcher); + + dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event)); + + let error: unknown; + let response: proto.OutboundMessage_CompileResponse | undefined; + dispatcher.sendCompileRequest(request, (error_, response_) => { + this.dispatchers.delete(dispatcher); + // Reset the compilation ID when the compiler goes idle (no active + // dispatchers) to avoid overflowing it. + // https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794 + if (this.dispatchers.size === 0) this.compilationId = 1; + if (error_) { + error = error_; + } else { + response = response_; + } + }); + + for (;;) { + if (!this.yield()) { + throw utils.compilerError('Embedded compiler exited unexpectedly.'); + } + + if (error) throw error; + if (response) return handleCompileResponse(response); } - - if (error) throw error; - if (response) return handleCompileResponse(response); + } finally { + activeDeprecationOptions.delete(optionsKey); } } diff --git a/lib/src/deprecations.ts b/lib/src/deprecations.ts index 05ce3c37..56676083 100644 --- a/lib/src/deprecations.ts +++ b/lib/src/deprecations.ts @@ -2,7 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import {Deprecation, DeprecationOrId} from './vendor/sass'; +import {Deprecation, DeprecationOrId, Options} from './vendor/sass'; import {Version} from './version'; export {deprecations} from './vendor/deprecations'; @@ -25,29 +25,39 @@ export function getDeprecationIds( }); } +/** + * Map between active compilations and the deprecation options they use. + * + * This is used to determine which options to use when handling host-side + * deprecation warnings that aren't explicitly tied to a particular compilation. + */ +export const activeDeprecationOptions: Map = + new Map(); + /** * Shorthand for the subset of options related to deprecations. */ -export type DeprecationOptions = { - fatalDeprecations?: (DeprecationOrId | Version)[]; - futureDeprecations?: DeprecationOrId[]; - silenceDeprecations?: DeprecationOrId[]; -}; +export type DeprecationOptions = Pick< + Options<'sync'>, + 'fatalDeprecations' | 'futureDeprecations' | 'silenceDeprecations' +>; /** * Handles a host-side deprecation warning, either emitting a warning, throwing - * and error, or doing nothing depending on the deprecation options used. + * an error, or doing nothing depending on the deprecation options used. + * + * If no specific deprecation options are passed here, then options will be + * determined based on the options of the active compilations. */ export function warnForHostSideDeprecation( message: string, deprecation: Deprecation, options?: DeprecationOptions ): void { + if (options) throw Error(message); if ( deprecation.status === 'future' && - !getDeprecationIds(options?.futureDeprecations ?? []).includes( - deprecation.id - ) + !isEnabledFuture(deprecation, options) ) { return; } @@ -55,23 +65,63 @@ export function warnForHostSideDeprecation( if (isFatal(deprecation, options)) { throw Error(fullMessage); } - if ( - !getDeprecationIds(options?.silenceDeprecations ?? []).includes( - deprecation.id - ) - ) { + if (!isSilent(deprecation, options)) { console.warn(fullMessage); } } +/** + * Checks whether the given deprecation is included in the given list of silent + * deprecations or is silenced by at least one active compilation. + */ +function isSilent( + deprecation: Deprecation, + options?: DeprecationOptions +): boolean { + if (!options) { + for (const potentialOptions of activeDeprecationOptions.values()) { + if (isSilent(deprecation, potentialOptions)) return true; + } + return false; + } + return getDeprecationIds(options?.silenceDeprecations ?? []).includes( + deprecation.id + ); +} + +/** + * Checks whether the given deprecation is included in the given list of future + * deprecations that should be enabled or is enabled in all active compilations. + */ +function isEnabledFuture( + deprecation: Deprecation, + options?: DeprecationOptions +): boolean { + if (!options) { + for (const potentialOptions of activeDeprecationOptions.values()) { + if (!isEnabledFuture(deprecation, potentialOptions)) return false; + } + return activeDeprecationOptions.size > 0; + } + return getDeprecationIds(options?.futureDeprecations ?? []).includes( + deprecation.id + ); +} + /** * Checks whether the given deprecation is included in the given list of - * fatal deprecations. + * fatal deprecations or is marked as fatal in all active compilations. */ function isFatal( deprecation: Deprecation, options?: DeprecationOptions ): boolean { + if (!options) { + for (const potentialOptions of activeDeprecationOptions.values()) { + if (!isFatal(deprecation, potentialOptions)) return false; + } + return activeDeprecationOptions.size > 0; + } const versionNumber = deprecation.deprecatedIn === null ? null @@ -90,7 +140,7 @@ function isFatal( } else if (typeof fatal === 'string') { if (fatal === deprecation.id) return true; } else { - if (fatal.id === deprecation.id) return true; + if ((fatal as Deprecation).id === deprecation.id) return true; } } return false; From eba8a3236a176bef6fcefc74e72de893a3e4cdc7 Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Mon, 16 Sep 2024 15:24:46 -0700 Subject: [PATCH 3/4] Remove debug line that was causing errors --- lib/src/deprecations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/deprecations.ts b/lib/src/deprecations.ts index 56676083..579baeeb 100644 --- a/lib/src/deprecations.ts +++ b/lib/src/deprecations.ts @@ -54,7 +54,6 @@ export function warnForHostSideDeprecation( deprecation: Deprecation, options?: DeprecationOptions ): void { - if (options) throw Error(message); if ( deprecation.status === 'future' && !isEnabledFuture(deprecation, options) From f05b657effcf6356ab2ca3519962cd5e57e0e5a5 Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Tue, 17 Sep 2024 10:32:44 -0700 Subject: [PATCH 4/4] Code review --- lib/src/deprecations.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/deprecations.ts b/lib/src/deprecations.ts index 579baeeb..e284b562 100644 --- a/lib/src/deprecations.ts +++ b/lib/src/deprecations.ts @@ -31,7 +31,7 @@ export function getDeprecationIds( * This is used to determine which options to use when handling host-side * deprecation warnings that aren't explicitly tied to a particular compilation. */ -export const activeDeprecationOptions: Map = +export const activeDeprecationOptions: Map = new Map(); /** @@ -83,7 +83,7 @@ function isSilent( } return false; } - return getDeprecationIds(options?.silenceDeprecations ?? []).includes( + return getDeprecationIds(options.silenceDeprecations ?? []).includes( deprecation.id ); } @@ -102,7 +102,7 @@ function isEnabledFuture( } return activeDeprecationOptions.size > 0; } - return getDeprecationIds(options?.futureDeprecations ?? []).includes( + return getDeprecationIds(options.futureDeprecations ?? []).includes( deprecation.id ); } @@ -127,7 +127,7 @@ function isFatal( : deprecation.deprecatedIn.major * 1000000 + deprecation.deprecatedIn.minor * 1000 + deprecation.deprecatedIn.patch; - for (const fatal of options?.fatalDeprecations ?? []) { + for (const fatal of options.fatalDeprecations ?? []) { if (fatal instanceof Version) { if (versionNumber === null) continue; if (