Skip to content

Commit c4824db

Browse files
authored
Emit deprecation warnings for legacy JS API (#331)
Also support deprecation flags for color-4-api deprecation
1 parent f8e1a7b commit c4824db

File tree

5 files changed

+233
-87
lines changed

5 files changed

+233
-87
lines changed

lib/src/compiler/async.ts

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
newCompileStringRequest,
1818
} from './utils';
1919
import {compilerCommand} from '../compiler-path';
20+
import {activeDeprecationOptions} from '../deprecations';
2021
import {FunctionRegistry} from '../function-registry';
2122
import {ImporterRegistry} from '../importer-registry';
2223
import {MessageTransformer} from '../message-transformer';
@@ -102,38 +103,44 @@ export class AsyncCompiler {
102103
importers: ImporterRegistry<'async'>,
103104
options?: OptionsWithLegacy<'async'> & {legacy?: boolean}
104105
): Promise<CompileResult> {
105-
const functions = new FunctionRegistry(options?.functions);
106-
107-
const dispatcher = createDispatcher<'async'>(
108-
this.compilationId++,
109-
this.messageTransformer,
110-
{
111-
handleImportRequest: request => importers.import(request),
112-
handleFileImportRequest: request => importers.fileImport(request),
113-
handleCanonicalizeRequest: request => importers.canonicalize(request),
114-
handleFunctionCallRequest: request => functions.call(request),
115-
}
116-
);
117-
dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event));
118-
119-
const compilation = new Promise<proto.OutboundMessage_CompileResponse>(
120-
(resolve, reject) =>
121-
dispatcher.sendCompileRequest(request, (err, response) => {
122-
this.compilations.delete(compilation);
123-
// Reset the compilation ID when the compiler goes idle (no active
124-
// compilations) to avoid overflowing it.
125-
// https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794
126-
if (this.compilations.size === 0) this.compilationId = 1;
127-
if (err) {
128-
reject(err);
129-
} else {
130-
resolve(response!);
131-
}
132-
})
133-
);
134-
this.compilations.add(compilation);
106+
const optionsKey = Symbol();
107+
activeDeprecationOptions.set(optionsKey, options ?? {});
108+
try {
109+
const functions = new FunctionRegistry(options?.functions);
110+
111+
const dispatcher = createDispatcher<'async'>(
112+
this.compilationId++,
113+
this.messageTransformer,
114+
{
115+
handleImportRequest: request => importers.import(request),
116+
handleFileImportRequest: request => importers.fileImport(request),
117+
handleCanonicalizeRequest: request => importers.canonicalize(request),
118+
handleFunctionCallRequest: request => functions.call(request),
119+
}
120+
);
121+
dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event));
122+
123+
const compilation = new Promise<proto.OutboundMessage_CompileResponse>(
124+
(resolve, reject) =>
125+
dispatcher.sendCompileRequest(request, (err, response) => {
126+
this.compilations.delete(compilation);
127+
// Reset the compilation ID when the compiler goes idle (no active
128+
// compilations) to avoid overflowing it.
129+
// https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794
130+
if (this.compilations.size === 0) this.compilationId = 1;
131+
if (err) {
132+
reject(err);
133+
} else {
134+
resolve(response!);
135+
}
136+
})
137+
);
138+
this.compilations.add(compilation);
135139

136-
return handleCompileResponse(await compilation);
140+
return handleCompileResponse(await compilation);
141+
} finally {
142+
activeDeprecationOptions.delete(optionsKey);
143+
}
137144
}
138145

139146
/** Initialize resources shared across compilations. */

lib/src/compiler/sync.ts

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
newCompileStringRequest,
1515
} from './utils';
1616
import {compilerCommand} from '../compiler-path';
17+
import {activeDeprecationOptions} from '../deprecations';
1718
import {Dispatcher} from '../dispatcher';
1819
import {FunctionRegistry} from '../function-registry';
1920
import {ImporterRegistry} from '../importer-registry';
@@ -108,44 +109,50 @@ export class Compiler {
108109
importers: ImporterRegistry<'sync'>,
109110
options?: OptionsWithLegacy<'sync'>
110111
): CompileResult {
111-
const functions = new FunctionRegistry(options?.functions);
112-
113-
const dispatcher = createDispatcher<'sync'>(
114-
this.compilationId++,
115-
this.messageTransformer,
116-
{
117-
handleImportRequest: request => importers.import(request),
118-
handleFileImportRequest: request => importers.fileImport(request),
119-
handleCanonicalizeRequest: request => importers.canonicalize(request),
120-
handleFunctionCallRequest: request => functions.call(request),
121-
}
122-
);
123-
this.dispatchers.add(dispatcher);
124-
125-
dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event));
126-
127-
let error: unknown;
128-
let response: proto.OutboundMessage_CompileResponse | undefined;
129-
dispatcher.sendCompileRequest(request, (error_, response_) => {
130-
this.dispatchers.delete(dispatcher);
131-
// Reset the compilation ID when the compiler goes idle (no active
132-
// dispatchers) to avoid overflowing it.
133-
// https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794
134-
if (this.dispatchers.size === 0) this.compilationId = 1;
135-
if (error_) {
136-
error = error_;
137-
} else {
138-
response = response_;
139-
}
140-
});
141-
142-
for (;;) {
143-
if (!this.yield()) {
144-
throw utils.compilerError('Embedded compiler exited unexpectedly.');
112+
const optionsKey = Symbol();
113+
activeDeprecationOptions.set(optionsKey, options ?? {});
114+
try {
115+
const functions = new FunctionRegistry(options?.functions);
116+
117+
const dispatcher = createDispatcher<'sync'>(
118+
this.compilationId++,
119+
this.messageTransformer,
120+
{
121+
handleImportRequest: request => importers.import(request),
122+
handleFileImportRequest: request => importers.fileImport(request),
123+
handleCanonicalizeRequest: request => importers.canonicalize(request),
124+
handleFunctionCallRequest: request => functions.call(request),
125+
}
126+
);
127+
this.dispatchers.add(dispatcher);
128+
129+
dispatcher.logEvents$.subscribe(event => handleLogEvent(options, event));
130+
131+
let error: unknown;
132+
let response: proto.OutboundMessage_CompileResponse | undefined;
133+
dispatcher.sendCompileRequest(request, (error_, response_) => {
134+
this.dispatchers.delete(dispatcher);
135+
// Reset the compilation ID when the compiler goes idle (no active
136+
// dispatchers) to avoid overflowing it.
137+
// https://github.com/sass/embedded-host-node/pull/261#discussion_r1429266794
138+
if (this.dispatchers.size === 0) this.compilationId = 1;
139+
if (error_) {
140+
error = error_;
141+
} else {
142+
response = response_;
143+
}
144+
});
145+
146+
for (;;) {
147+
if (!this.yield()) {
148+
throw utils.compilerError('Embedded compiler exited unexpectedly.');
149+
}
150+
151+
if (error) throw error;
152+
if (response) return handleCompileResponse(response);
145153
}
146-
147-
if (error) throw error;
148-
if (response) return handleCompileResponse(response);
154+
} finally {
155+
activeDeprecationOptions.delete(optionsKey);
149156
}
150157
}
151158

lib/src/deprecations.ts

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5-
import {DeprecationOrId} from './vendor/sass';
5+
import {Deprecation, DeprecationOrId, Options} from './vendor/sass';
66
import {Version} from './version';
77

88
export {deprecations} from './vendor/deprecations';
@@ -15,12 +15,132 @@ export {Deprecation, DeprecationOrId, DeprecationStatus} from './vendor/sass';
1515
export function getDeprecationIds(
1616
arr: (DeprecationOrId | Version)[]
1717
): string[] {
18-
return arr.flatMap(item => {
18+
return arr.map(item => {
1919
if (item instanceof Version) {
20-
return arr.map(item => item.toString());
20+
return item.toString();
2121
} else if (typeof item === 'string') {
2222
return item;
2323
}
2424
return item.id;
2525
});
2626
}
27+
28+
/**
29+
* Map between active compilations and the deprecation options they use.
30+
*
31+
* This is used to determine which options to use when handling host-side
32+
* deprecation warnings that aren't explicitly tied to a particular compilation.
33+
*/
34+
export const activeDeprecationOptions: Map<Symbol, DeprecationOptions> =
35+
new Map();
36+
37+
/**
38+
* Shorthand for the subset of options related to deprecations.
39+
*/
40+
export type DeprecationOptions = Pick<
41+
Options<'sync'>,
42+
'fatalDeprecations' | 'futureDeprecations' | 'silenceDeprecations'
43+
>;
44+
45+
/**
46+
* Handles a host-side deprecation warning, either emitting a warning, throwing
47+
* an error, or doing nothing depending on the deprecation options used.
48+
*
49+
* If no specific deprecation options are passed here, then options will be
50+
* determined based on the options of the active compilations.
51+
*/
52+
export function warnForHostSideDeprecation(
53+
message: string,
54+
deprecation: Deprecation,
55+
options?: DeprecationOptions
56+
): void {
57+
if (
58+
deprecation.status === 'future' &&
59+
!isEnabledFuture(deprecation, options)
60+
) {
61+
return;
62+
}
63+
const fullMessage = `Deprecation [${deprecation.id}]: ${message}`;
64+
if (isFatal(deprecation, options)) {
65+
throw Error(fullMessage);
66+
}
67+
if (!isSilent(deprecation, options)) {
68+
console.warn(fullMessage);
69+
}
70+
}
71+
72+
/**
73+
* Checks whether the given deprecation is included in the given list of silent
74+
* deprecations or is silenced by at least one active compilation.
75+
*/
76+
function isSilent(
77+
deprecation: Deprecation,
78+
options?: DeprecationOptions
79+
): boolean {
80+
if (!options) {
81+
for (const potentialOptions of activeDeprecationOptions.values()) {
82+
if (isSilent(deprecation, potentialOptions)) return true;
83+
}
84+
return false;
85+
}
86+
return getDeprecationIds(options.silenceDeprecations ?? []).includes(
87+
deprecation.id
88+
);
89+
}
90+
91+
/**
92+
* Checks whether the given deprecation is included in the given list of future
93+
* deprecations that should be enabled or is enabled in all active compilations.
94+
*/
95+
function isEnabledFuture(
96+
deprecation: Deprecation,
97+
options?: DeprecationOptions
98+
): boolean {
99+
if (!options) {
100+
for (const potentialOptions of activeDeprecationOptions.values()) {
101+
if (!isEnabledFuture(deprecation, potentialOptions)) return false;
102+
}
103+
return activeDeprecationOptions.size > 0;
104+
}
105+
return getDeprecationIds(options.futureDeprecations ?? []).includes(
106+
deprecation.id
107+
);
108+
}
109+
110+
/**
111+
* Checks whether the given deprecation is included in the given list of
112+
* fatal deprecations or is marked as fatal in all active compilations.
113+
*/
114+
function isFatal(
115+
deprecation: Deprecation,
116+
options?: DeprecationOptions
117+
): boolean {
118+
if (!options) {
119+
for (const potentialOptions of activeDeprecationOptions.values()) {
120+
if (!isFatal(deprecation, potentialOptions)) return false;
121+
}
122+
return activeDeprecationOptions.size > 0;
123+
}
124+
const versionNumber =
125+
deprecation.deprecatedIn === null
126+
? null
127+
: deprecation.deprecatedIn.major * 1000000 +
128+
deprecation.deprecatedIn.minor * 1000 +
129+
deprecation.deprecatedIn.patch;
130+
for (const fatal of options.fatalDeprecations ?? []) {
131+
if (fatal instanceof Version) {
132+
if (versionNumber === null) continue;
133+
if (
134+
versionNumber <=
135+
fatal.major * 1000000 + fatal.minor * 1000 + fatal.patch
136+
) {
137+
return true;
138+
}
139+
} else if (typeof fatal === 'string') {
140+
if (fatal === deprecation.id) return true;
141+
} else {
142+
if ((fatal as Deprecation).id === deprecation.id) return true;
143+
}
144+
}
145+
return false;
146+
}

lib/src/legacy/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
compileString,
1515
compileStringAsync,
1616
} from '../compile';
17+
import {deprecations, warnForHostSideDeprecation} from '../deprecations';
1718
import {
1819
SyncBoolean,
1920
fileUrlToPathCrossPlatform,
@@ -50,6 +51,11 @@ export function render(
5051
options = adjustOptions(options);
5152

5253
const start = Date.now();
54+
warnForHostSideDeprecation(
55+
'The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.',
56+
deprecations['legacy-js-api'],
57+
options
58+
);
5359
const compileSass = isStringOptions(options)
5460
? compileStringAsync(options.data, convertStringOptions(options, false))
5561
: compileAsync(options.file, convertOptions(options, false));
@@ -68,6 +74,11 @@ export function renderSync(options: LegacyOptions<'sync'>): LegacyResult {
6874
const start = Date.now();
6975
try {
7076
options = adjustOptions(options);
77+
warnForHostSideDeprecation(
78+
'The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.',
79+
deprecations['legacy-js-api'],
80+
options
81+
);
7182
const result = isStringOptions(options)
7283
? compileString(options.data, convertStringOptions(options, true))
7384
: compile(options.file, convertOptions(options, true));

0 commit comments

Comments
 (0)