Skip to content

Commit

Permalink
Implement sass --embedded in pure JS mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ntkme committed Oct 26, 2024
1 parent f6e2e26 commit 2b23fac
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 30 deletions.
19 changes: 16 additions & 3 deletions lib/src/compiler-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ export const compilerCommand = (() => {
`sass-embedded-${platform}-${arch}/dart-sass/src/sass.snapshot`
),
];
} catch (ignored) {
// ignored
} catch (e) {
if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) {
throw e;
}
}

try {
Expand All @@ -70,10 +72,21 @@ export const compilerCommand = (() => {
}
}

try {
return [
process.execPath,
p.join(p.dirname(require.resolve('sass')), 'sass.js'),
];
} catch (e: unknown) {
if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) {
throw e;
}
}

throw new Error(
"Embedded Dart Sass couldn't find the embedded compiler executable. " +
'Please make sure the optional dependency ' +
`sass-embedded-${platform}-${arch} is installed in ` +
`sass-embedded-${platform}-${arch} or sass is installed in ` +
'node_modules.'
);
})();
7 changes: 7 additions & 0 deletions lib/src/embedded/index.mjs
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;
46 changes: 46 additions & 0 deletions lib/src/embedded/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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(
toJson(
proto.OutboundMessage_VersionResponseSchema,
WorkerDispatcher.versionResponse()
)
);
} 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);
}
}
60 changes: 60 additions & 0 deletions lib/src/embedded/reusable_worker.ts
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}.`
);
}
}
50 changes: 50 additions & 0 deletions lib/src/embedded/utils.ts
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;
}
}
175 changes: 175 additions & 0 deletions lib/src/embedded/worker_dispatcher.ts
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',
});
}
}
Loading

0 comments on commit 2b23fac

Please sign in to comment.