-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathsync.ts
More file actions
221 lines (192 loc) · 7.06 KB
/
sync.ts
File metadata and controls
221 lines (192 loc) · 7.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Copyright 2024 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 path from 'path';
import {Subject} from 'rxjs';
import * as sync_child_process from 'sync-child-process';
import {
OptionsWithLegacy,
createDispatcher,
handleCompileResponse,
handleLogEvent,
newCompilePathRequest,
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';
import {MessageTransformer} from '../message-transformer';
import {PacketTransformer} from '../packet-transformer';
import * as utils from '../utils';
import * as proto from '../vendor/embedded_sass_pb';
import {CompileResult} from '../vendor/sass/compile';
import {Options} from '../vendor/sass/options';
/**
* Flag allowing the constructor passed by `initCompiler` so we can
* differentiate and throw an error if the `Compiler` is constructed via `new
* Compiler`.
*/
const initFlag = Symbol();
/** A synchronous wrapper for the embedded Sass compiler */
export class Compiler {
/** The underlying process that's being wrapped. */
private readonly process = (() => {
let command = compilerCommand[0];
let args = [...compilerCommand.slice(1), '--embedded'];
const options: sync_child_process.Options = {
// Use the command's cwd so the compiler survives the removal of the
// current working directory.
// https://github.com/sass/embedded-host-node/pull/261#discussion_r1438712923
cwd: path.dirname(compilerCommand[0]),
windowsHide: true,
};
// Node forbids launching .bat and .cmd without a shell due to CVE-2024-27980,
// and DEP0190 forbids passing an argument list *with* shell: true. To work
// around this, we have to manually concatenate the arguments.
if (['.bat', '.cmd'].includes(path.extname(command).toLowerCase())) {
command = `${command} ${args!.join(' ')}`;
args = [];
options.shell = true;
}
return new sync_child_process.SyncChildProcess(command, args, options);
})();
/** The next compilation ID. */
private compilationId = 1;
/** A list of active dispatchers. */
private readonly dispatchers: Set<Dispatcher<'sync'>> = new Set();
/** The buffers emitted by the child process's stdout. */
private readonly stdout$ = new Subject<Buffer>();
/** The buffers emitted by the child process's stderr. */
private readonly stderr$ = new Subject<Buffer>();
/** Whether the underlying compiler has already exited. */
private disposed = false;
/** Reusable message transformer for all compilations. */
private readonly messageTransformer: MessageTransformer;
/** Writes `buffer` to the child process's stdin. */
private writeStdin(buffer: Buffer): void {
this.process.stdin.write(buffer);
}
/** Yields the next event from the underlying process. */
private yield(): boolean {
const result = this.process.next();
if (result.done) {
this.disposed = true;
return false;
}
const event = result.value;
switch (event.type) {
case 'stdout':
this.stdout$.next(event.data);
return true;
case 'stderr':
this.stderr$.next(event.data);
return true;
}
}
/** Blocks until the underlying process exits. */
private yieldUntilExit(): void {
while (!this.disposed) {
this.yield();
}
}
/**
* Sends a compile request to the child process and returns the CompileResult.
* Throws if there were any protocol or compilation errors.
*/
private compileRequestSync(
request: proto.InboundMessage_CompileRequest,
importers: ImporterRegistry<'sync'>,
options?: OptionsWithLegacy<'sync'>,
): CompileResult {
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);
}
} finally {
activeDeprecationOptions.delete(optionsKey);
}
}
/** Guards against using a disposed compiler. */
private throwIfDisposed(): void {
if (this.disposed) {
throw utils.compilerError('Sync compiler has already been disposed.');
}
}
/** Initialize resources shared across compilations. */
constructor(flag: symbol | undefined) {
if (flag !== initFlag) {
throw utils.compilerError(
'Compiler can not be directly constructed. ' +
'Please use `sass.initAsyncCompiler()` instead.',
);
}
this.stderr$.subscribe(data => process.stderr.write(data));
const packetTransformer = new PacketTransformer(this.stdout$, buffer => {
this.writeStdin(buffer);
});
this.messageTransformer = new MessageTransformer(
packetTransformer.outboundProtobufs$,
packet => packetTransformer.writeInboundProtobuf(packet),
);
}
compile(path: string, options?: Options<'sync'>): CompileResult {
this.throwIfDisposed();
const importers = new ImporterRegistry(options);
return this.compileRequestSync(
newCompilePathRequest(path, importers, options),
importers,
options,
);
}
compileString(source: string, options?: Options<'sync'>): CompileResult {
this.throwIfDisposed();
const importers = new ImporterRegistry(options);
return this.compileRequestSync(
newCompileStringRequest(source, importers, options),
importers,
options,
);
}
dispose(): void {
this.process.stdin.end();
this.yieldUntilExit();
}
}
export function initCompiler(): Compiler {
return new Compiler(initFlag);
}