-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement
sass --embedded
in pure JS mode
- Loading branch information
Showing
8 changed files
with
359 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// 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 embedded from './index.js'; | ||
|
||
export const main = embedded.main; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// 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 {MessagePort, isMainThread, workerData} from 'worker_threads'; | ||
import {toJson} from '@bufbuild/protobuf'; | ||
|
||
import {SyncMessagePort} from '../sync-process/sync-message-port'; | ||
import {WorkerDispatcher} from './worker_dispatcher'; | ||
import * as proto from '../vendor/embedded_sass_pb'; | ||
|
||
export function main( | ||
spawnCompilationDispatcher: ( | ||
mailbox: SyncMessagePort, | ||
sendPort: MessagePort | ||
) => void | ||
): void { | ||
if (isMainThread) { | ||
if (process.argv.length > 3) { | ||
if (process.argv[3] === '--version') { | ||
console.log( | ||
JSON.stringify( | ||
toJson( | ||
proto.OutboundMessage_VersionResponseSchema, | ||
WorkerDispatcher.versionResponse() | ||
), | ||
null, | ||
2 | ||
) | ||
); | ||
} else { | ||
console.error( | ||
'sass --embedded is not intended to be executed with additional arguments.\n' + | ||
'See https://github.com/sass/dart-sass#embedded-dart-sass for details.' | ||
); | ||
process.exitCode = 64; | ||
} | ||
return; | ||
} | ||
|
||
new WorkerDispatcher().listen(); | ||
} else { | ||
const port = workerData.port as MessagePort; | ||
spawnCompilationDispatcher(new SyncMessagePort(port), { | ||
postMessage(buffer: Uint8Array): void { | ||
port.postMessage(buffer, [buffer.buffer]); | ||
}, | ||
} as MessagePort); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// 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 {MessagePort, Worker} from 'worker_threads'; | ||
|
||
import {SyncMessagePort} from '../sync-process/sync-message-port'; | ||
|
||
export class ReusableWorker { | ||
private readonly worker: Worker; | ||
|
||
private readonly receivePort: MessagePort; | ||
|
||
private readonly sendPort: SyncMessagePort; | ||
|
||
private onMessage = this.defaultOnMessage; | ||
|
||
constructor(path: string) { | ||
const {port1, port2} = SyncMessagePort.createChannel(); | ||
this.worker = new Worker(path, { | ||
workerData: {port: port2}, | ||
transferList: [port2], | ||
argv: process.argv.slice(2), | ||
}); | ||
this.receivePort = port1; | ||
this.sendPort = new SyncMessagePort(port1); | ||
|
||
this.receivePort.on('message', value => this.onMessage(value)); | ||
} | ||
|
||
borrow(listener: (value: Uint8Array) => void): void { | ||
if (this.onMessage !== this.defaultOnMessage) { | ||
throw new Error('ReusableWorker has already been borrowed.'); | ||
} | ||
this.onMessage = listener; | ||
} | ||
|
||
release(): void { | ||
if (this.onMessage === this.defaultOnMessage) { | ||
throw new Error('ReusableWorker has not been borrowed.'); | ||
} | ||
this.onMessage = this.defaultOnMessage; | ||
} | ||
|
||
send(value: Uint8Array): void { | ||
this.sendPort.postMessage(value, [value.buffer]); | ||
} | ||
|
||
terminate(): void { | ||
this.sendPort.close(); | ||
this.worker.terminate(); | ||
this.receivePort.close(); | ||
} | ||
|
||
private defaultOnMessage(value: Uint8Array): void { | ||
throw new Error( | ||
`Shouldn't receive a message before being borrowed: ${value}.` | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// 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 {create} from '@bufbuild/protobuf'; | ||
|
||
import * as proto from '../vendor/embedded_sass_pb'; | ||
|
||
export const errorId = 0xffffffff; | ||
|
||
export function paramsError(message: string): proto.ProtocolError { | ||
return create(proto.ProtocolErrorSchema, { | ||
id: errorId, | ||
type: proto.ProtocolErrorType.PARAMS, | ||
message: message, | ||
}); | ||
} | ||
|
||
export function parseError(message: string): proto.ProtocolError { | ||
return create(proto.ProtocolErrorSchema, { | ||
type: proto.ProtocolErrorType.PARSE, | ||
message: message, | ||
}); | ||
} | ||
|
||
export function handleError( | ||
error: Error | proto.ProtocolError, | ||
{messageId}: {messageId?: number} = {} | ||
): proto.ProtocolError { | ||
if (error instanceof Error) { | ||
const errorMessage = `${error.message}\n${error.stack}`; | ||
process.stderr.write(`Internal compiler error: ${errorMessage}`); | ||
process.exitCode = 70; // EX_SOFTWARE | ||
return create(proto.ProtocolErrorSchema, { | ||
id: messageId ?? errorId, | ||
type: proto.ProtocolErrorType.INTERNAL, | ||
message: errorMessage, | ||
}); | ||
} else { | ||
error.id = messageId ?? errorId; | ||
process.stderr.write( | ||
`Host caused ${proto.ProtocolErrorType[error.type].toLowerCase()} error` | ||
); | ||
if (error.id !== errorId) process.stderr.write(` with request ${error.id}`); | ||
process.stderr.write(`: ${error.message}\n`); | ||
// PROTOCOL error from https://bit.ly/2poTt90 | ||
process.exitCode = 76; // EX_PROTOCOL | ||
return error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// 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 {Observable} from 'rxjs'; | ||
import {takeUntil} from 'rxjs/operators'; | ||
import {create, fromBinary, toBinary} from '@bufbuild/protobuf'; | ||
import * as varint from 'varint'; | ||
|
||
import * as pkg from '../../../package.json'; | ||
import {PacketTransformer} from '../packet-transformer'; | ||
import {ReusableWorker} from './reusable_worker'; | ||
import {errorId, handleError, paramsError, parseError} from './utils'; | ||
import * as proto from '../vendor/embedded_sass_pb'; | ||
|
||
export class WorkerDispatcher { | ||
private readonly allWorkers: ReusableWorker[] = []; | ||
|
||
private readonly inactiveWorkers: ReusableWorker[] = []; | ||
|
||
private readonly activeWorkers = new Map<number, ReusableWorker>(); | ||
|
||
private readonly stdin$ = new Observable<Buffer>(observer => { | ||
process.stdin.on('data', buffer => observer.next(buffer)); | ||
}).pipe( | ||
takeUntil( | ||
new Promise(resolve => { | ||
process.stdin.on('close', () => resolve(undefined)); | ||
}) | ||
) | ||
); | ||
|
||
private readonly packetTransformer = new PacketTransformer( | ||
this.stdin$, | ||
buffer => process.stdout.write(buffer) | ||
); | ||
|
||
listen(): void { | ||
this.packetTransformer.protobufs$.subscribe({ | ||
next: (buffer: Uint8Array) => { | ||
let compilationId: number; | ||
try { | ||
compilationId = varint.decode(buffer); | ||
} catch (error) { | ||
throw parseError(`Invalid compilation ID varint: ${error}`); | ||
} | ||
|
||
try { | ||
if (compilationId !== 0) { | ||
if (this.activeWorkers.has(compilationId)) { | ||
const worker = this.activeWorkers.get(compilationId)!; | ||
worker.send(buffer); | ||
} else { | ||
const worker = this.getWorker(compilationId); | ||
this.activeWorkers.set(compilationId, worker); | ||
worker.send(buffer); | ||
} | ||
return; | ||
} | ||
|
||
let message; | ||
try { | ||
message = fromBinary( | ||
proto.InboundMessageSchema, | ||
new Uint8Array(buffer.buffer, varint.decode.bytes) | ||
); | ||
} catch (error) { | ||
throw parseError(`Invalid protobuf: ${error}`); | ||
} | ||
|
||
if (message.message.case !== 'versionRequest') { | ||
throw paramsError( | ||
`Only VersionRequest may have wire ID 0, was ${message.message.case}.` | ||
); | ||
} | ||
const request = message.message.value; | ||
const response = WorkerDispatcher.versionResponse(); | ||
response.id = request.id; | ||
this.send( | ||
0, | ||
create(proto.OutboundMessageSchema, { | ||
message: { | ||
case: 'versionResponse', | ||
value: response, | ||
}, | ||
}) | ||
); | ||
} catch (error) { | ||
this.handleError(error); | ||
} | ||
}, | ||
complete: () => { | ||
this.allWorkers.forEach(worker => worker.terminate()); | ||
}, | ||
error: error => { | ||
this.handleError(parseError(error.message)); | ||
}, | ||
}); | ||
} | ||
|
||
private getWorker(compilationId: number): ReusableWorker { | ||
let worker: ReusableWorker; | ||
if (this.inactiveWorkers.length > 0) { | ||
worker = this.inactiveWorkers.pop()!; | ||
} else { | ||
worker = new ReusableWorker(process.argv[1]); | ||
this.allWorkers.push(worker); | ||
} | ||
|
||
worker.borrow(buffer => { | ||
const category = buffer.at(0); | ||
const packet = Buffer.from(buffer.buffer, 1); | ||
|
||
switch (category) { | ||
case 0: | ||
this.packetTransformer.writeProtobuf(packet); | ||
break; | ||
case 1: | ||
this.activeWorkers.delete(compilationId); | ||
worker.release(); | ||
this.inactiveWorkers.push(worker); | ||
this.packetTransformer.writeProtobuf(packet); | ||
break; | ||
case 2: { | ||
this.packetTransformer.writeProtobuf(packet); | ||
/* eslint-disable-next-line n/no-process-exit */ | ||
process.exit(); | ||
} | ||
} | ||
}); | ||
|
||
return worker; | ||
} | ||
|
||
private handleError( | ||
error: Error | proto.ProtocolError, | ||
{ | ||
compilationId, | ||
messageId, | ||
}: {compilationId?: number; messageId?: number} = {} | ||
): void { | ||
this.sendError(compilationId ?? errorId, handleError(error, {messageId})); | ||
process.stdin.destroy(); | ||
} | ||
|
||
private send(compilationId: number, message: proto.OutboundMessage): void { | ||
const compilationIdLength = varint.encodingLength(compilationId); | ||
const encodedMessage = toBinary(proto.OutboundMessageSchema, message); | ||
const buffer = new Uint8Array(compilationIdLength + encodedMessage.length); | ||
varint.encode(compilationId, buffer); | ||
buffer.set(encodedMessage, compilationIdLength); | ||
this.packetTransformer.writeProtobuf(buffer); | ||
} | ||
|
||
private sendError(compilationId: number, error: proto.ProtocolError): void { | ||
this.send( | ||
compilationId, | ||
create(proto.OutboundMessageSchema, { | ||
message: { | ||
case: 'error', | ||
value: error, | ||
}, | ||
}) | ||
); | ||
} | ||
|
||
static versionResponse(): proto.OutboundMessage_VersionResponse { | ||
return create(proto.OutboundMessage_VersionResponseSchema, { | ||
protocolVersion: pkg['protocol-version'], | ||
compilerVersion: pkg['compiler-version'], | ||
implementationVersion: pkg['version'], | ||
implementationName: 'dart-sass', | ||
}); | ||
} | ||
} |
Oops, something went wrong.