-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Shared Resources] Embedded host implementation #261
Changes from 28 commits
e8032ae
eb23204
9e78051
4aaa65f
1caf32e
b41959e
7906f99
ebfddd9
c47a958
a6ca5d9
8ecb0e3
ba7c156
cdd32ca
fbbe407
c35c965
298daab
06974e1
7b7fbf7
e7e4868
297cf67
b03ef75
ddfb710
bf7cf3c
7138091
d79ada4
26a0338
55e64f2
612db59
329fa94
2556525
f5c2e8a
66a7f66
55a55ed
0f0b5aa
2b24ec4
a222ff2
84ae6a4
82819b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,42 +6,174 @@ import {spawn} from 'child_process'; | |
import {Observable} from 'rxjs'; | ||
import {takeUntil} from 'rxjs/operators'; | ||
|
||
import * as path from 'path'; | ||
import { | ||
OptionsWithLegacy, | ||
StringOptionsWithLegacy, | ||
createDispatcher, | ||
handleCompileResponse, | ||
handleLogEvent, | ||
newCompilePathRequest, | ||
newCompileStringRequest, | ||
} from './compiler'; | ||
import {compilerCommand} from './compiler-path'; | ||
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'; | ||
|
||
/** | ||
* An asynchronous wrapper for the embedded Sass compiler that exposes its stdio | ||
* streams as Observables. | ||
* Flag allowing the constructor passed by `initAsyncCompiler` so we can | ||
* differentiate and throw an error if the `AsyncCompiler` is constructed via | ||
* `new AsyncCompiler`. | ||
*/ | ||
export class AsyncEmbeddedCompiler { | ||
const initFlag = Symbol(); | ||
|
||
/** An asynchronous wrapper for the embedded Sass compiler */ | ||
export class AsyncCompiler { | ||
/** The underlying process that's being wrapped. */ | ||
private readonly process = spawn( | ||
compilerCommand[0], | ||
[...compilerCommand.slice(1), '--embedded'], | ||
{windowsHide: true} | ||
{cwd: path.dirname(compilerCommand[0]), windowsHide: true} | ||
); | ||
|
||
/** The next compilation ID. */ | ||
private compilationId = 1; | ||
|
||
/** A list of active compilations. */ | ||
private readonly compilations: Set< | ||
Promise<proto.OutboundMessage_CompileResponse> | ||
> = new Set(); | ||
|
||
/** Whether the underlying compiler has already exited. */ | ||
private disposed = false; | ||
|
||
/** Reusable message transformer for all compilations. */ | ||
private readonly messageTransformer: MessageTransformer; | ||
|
||
/** The child process's exit event. */ | ||
readonly exit$ = new Promise<number | null>(resolve => { | ||
private readonly exit$ = new Promise<number | null>(resolve => { | ||
this.process.on('exit', code => resolve(code)); | ||
}); | ||
|
||
/** The buffers emitted by the child process's stdout. */ | ||
readonly stdout$ = new Observable<Buffer>(observer => { | ||
private readonly stdout$ = new Observable<Buffer>(observer => { | ||
this.process.stdout.on('data', buffer => observer.next(buffer)); | ||
}).pipe(takeUntil(this.exit$)); | ||
|
||
/** The buffers emitted by the child process's stderr. */ | ||
readonly stderr$ = new Observable<Buffer>(observer => { | ||
private readonly stderr$ = new Observable<Buffer>(observer => { | ||
this.process.stderr.on('data', buffer => observer.next(buffer)); | ||
}).pipe(takeUntil(this.exit$)); | ||
|
||
/** Writes `buffer` to the child process's stdin. */ | ||
writeStdin(buffer: Buffer): void { | ||
private writeStdin(buffer: Buffer): void { | ||
this.process.stdin.write(buffer); | ||
} | ||
|
||
/** Kills the child process, cleaning up all associated Observables. */ | ||
close() { | ||
/** Guards against using a disposed compiler. */ | ||
private throwIfDisposed(): void { | ||
jerivas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (this.disposed) { | ||
throw utils.compilerError('Async compiler has already been disposed.'); | ||
} | ||
} | ||
|
||
/** | ||
* Sends a compile request to the child process and returns a Promise that | ||
* resolves with the CompileResult. Rejects the promise if there were any | ||
* protocol or compilation errors. | ||
*/ | ||
private async compileRequestAsync( | ||
request: proto.InboundMessage_CompileRequest, | ||
importers: ImporterRegistry<'async'>, | ||
options?: OptionsWithLegacy<'async'> & {legacy?: boolean} | ||
): Promise<CompileResult> { | ||
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<proto.OutboundMessage_CompileResponse>( | ||
(resolve, reject) => | ||
dispatcher.sendCompileRequest(request, (err, response) => { | ||
this.compilations.delete(compilation); | ||
if (this.compilations.size === 0) this.compilationId = 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably worth adding a comment here about avoiding unbounded growth as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 66a7f66 |
||
if (err) { | ||
reject(err); | ||
} else { | ||
resolve(response!); | ||
} | ||
}) | ||
); | ||
this.compilations.add(compilation); | ||
|
||
return handleCompileResponse(await compilation); | ||
} | ||
|
||
/** Initialize resources shared across compilations. */ | ||
constructor(flag: Symbol | undefined) { | ||
if (flag !== initFlag) { | ||
throw utils.compilerError( | ||
'AsyncCompiler can not be directly constructed. Please use `sass.initAsyncCompiler()` instead.' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: long line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 66a7f66 |
||
); | ||
} | ||
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) | ||
); | ||
} | ||
|
||
compileAsync( | ||
path: string, | ||
options?: OptionsWithLegacy<'async'> | ||
): Promise<CompileResult> { | ||
this.throwIfDisposed(); | ||
const importers = new ImporterRegistry(options); | ||
return this.compileRequestAsync( | ||
newCompilePathRequest(path, importers, options), | ||
importers, | ||
options | ||
); | ||
} | ||
|
||
compileStringAsync( | ||
source: string, | ||
options?: StringOptionsWithLegacy<'async'> | ||
): Promise<CompileResult> { | ||
this.throwIfDisposed(); | ||
const importers = new ImporterRegistry(options); | ||
return this.compileRequestAsync( | ||
newCompileStringRequest(source, importers, options), | ||
importers, | ||
options | ||
); | ||
} | ||
|
||
async dispose(): Promise<void> { | ||
this.disposed = true; | ||
await Promise.all(this.compilations); | ||
this.process.stdin.end(); | ||
await this.exit$; | ||
jamesnw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
export async function initAsyncCompiler(): Promise<AsyncCompiler> { | ||
return new AsyncCompiler(initFlag); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably a good idea to add a comment here explaining why we set the CWD explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 66a7f66