Skip to content

Commit 9683817

Browse files
committed
Implement sass --embedded in pure JS mode
1 parent f6e2e26 commit 9683817

File tree

8 files changed

+368
-30
lines changed

8 files changed

+368
-30
lines changed

lib/src/compiler-path.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ export const compilerCommand = (() => {
5353
`sass-embedded-${platform}-${arch}/dart-sass/src/sass.snapshot`
5454
),
5555
];
56-
} catch (ignored) {
57-
// ignored
56+
} catch (e) {
57+
if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) {
58+
throw e;
59+
}
5860
}
5961

6062
try {
@@ -70,10 +72,21 @@ export const compilerCommand = (() => {
7072
}
7173
}
7274

75+
try {
76+
return [
77+
process.execPath,
78+
p.join(p.dirname(require.resolve('sass')), 'sass.js')
79+
];
80+
} catch (e: unknown) {
81+
if (!(isErrnoException(e) && e.code === 'MODULE_NOT_FOUND')) {
82+
throw e;
83+
}
84+
}
85+
7386
throw new Error(
7487
"Embedded Dart Sass couldn't find the embedded compiler executable. " +
7588
'Please make sure the optional dependency ' +
76-
`sass-embedded-${platform}-${arch} is installed in ` +
89+
`sass-embedded-${platform}-${arch} or sass is installed in ` +
7790
'node_modules.'
7891
);
7992
})();

lib/src/embedded/index.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright 2024 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import * as embedded from './index.js';
6+
7+
export const main = embedded.main;

lib/src/embedded/index.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2024 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {MessagePort, isMainThread, workerData} from 'worker_threads';
6+
import {toJson} from '@bufbuild/protobuf';
7+
8+
import {SyncMessagePort} from '../sync-process/sync-message-port';
9+
import {WorkerDispatcher} from './worker_dispatcher';
10+
import * as proto from '../vendor/embedded_sass_pb';
11+
12+
export function main(
13+
spawnCompilationDispatcher: (
14+
mailbox: SyncMessagePort,
15+
sendPort: MessagePort
16+
) => void
17+
): void {
18+
if (isMainThread) {
19+
if (process.argv.length > 3) {
20+
if (process.argv[3] === '--version') {
21+
console.log(
22+
toJson(
23+
proto.OutboundMessage_VersionResponseSchema,
24+
WorkerDispatcher.versionResponse()
25+
)
26+
);
27+
} else {
28+
console.error(
29+
'sass --embedded is not intended to be executed with additional arguments.\n' +
30+
'See https://github.com/sass/dart-sass#embedded-dart-sass for details.'
31+
);
32+
process.exitCode = 64;
33+
}
34+
return;
35+
}
36+
37+
new WorkerDispatcher().listen();
38+
} else {
39+
const port = workerData.port as MessagePort;
40+
spawnCompilationDispatcher(new SyncMessagePort(port), {
41+
postMessage(buffer: Uint8Array): void {
42+
port.postMessage(buffer, [buffer.buffer]);
43+
},
44+
} as MessagePort);
45+
}
46+
}

lib/src/embedded/reusable_worker.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2024 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {MessagePort, Worker} from 'worker_threads';
6+
7+
import {SyncMessagePort} from '../sync-process/sync-message-port';
8+
9+
export class ReusableWorker {
10+
private readonly worker: Worker;
11+
12+
private readonly receivePort: MessagePort;
13+
14+
private readonly sendPort: SyncMessagePort;
15+
16+
private onMessage = this.defaultOnMessage;
17+
18+
constructor(path: string) {
19+
const {port1, port2} = SyncMessagePort.createChannel();
20+
this.worker = new Worker(path, {
21+
workerData: {port: port2},
22+
transferList: [port2],
23+
argv: process.argv.slice(2),
24+
});
25+
this.receivePort = port1;
26+
this.sendPort = new SyncMessagePort(port1);
27+
28+
this.receivePort.on('message', value => this.onMessage(value));
29+
}
30+
31+
borrow(listener: (value: Uint8Array) => void): void {
32+
if (this.onMessage !== this.defaultOnMessage) {
33+
throw new Error('ReusableWorker has already been borrowed.');
34+
}
35+
this.onMessage = listener;
36+
}
37+
38+
release(): void {
39+
if (this.onMessage === this.defaultOnMessage) {
40+
throw new Error('ReusableWorker has not been borrowed.');
41+
}
42+
this.onMessage = this.defaultOnMessage;
43+
}
44+
45+
send(value: Uint8Array): void {
46+
this.sendPort.postMessage(value, [value.buffer]);
47+
}
48+
49+
terminate(): void {
50+
this.sendPort.close();
51+
this.worker.terminate();
52+
this.receivePort.close();
53+
}
54+
55+
private defaultOnMessage(value: Uint8Array): void {
56+
throw new Error(
57+
`Shouldn't receive a message before being borrowed: ${value}.`
58+
);
59+
}
60+
}

lib/src/embedded/utils.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2024 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {create} from '@bufbuild/protobuf';
6+
7+
import * as proto from '../vendor/embedded_sass_pb';
8+
9+
export const errorId = 0xffffffff;
10+
11+
export function paramsError(message: string): proto.ProtocolError {
12+
return create(proto.ProtocolErrorSchema, {
13+
id: errorId,
14+
type: proto.ProtocolErrorType.PARAMS,
15+
message: message,
16+
});
17+
}
18+
19+
export function parseError(message: string): proto.ProtocolError {
20+
return create(proto.ProtocolErrorSchema, {
21+
type: proto.ProtocolErrorType.PARSE,
22+
message: message,
23+
});
24+
}
25+
26+
export function handleError(
27+
error: Error | proto.ProtocolError,
28+
{messageId}: {messageId?: number} = {}
29+
): proto.ProtocolError {
30+
if (error instanceof Error) {
31+
const errorMessage = `${error.message}\n${error.stack}`;
32+
process.stderr.write(`Internal compiler error: ${errorMessage}`);
33+
process.exitCode = 70; // EX_SOFTWARE
34+
return create(proto.ProtocolErrorSchema, {
35+
id: messageId ?? errorId,
36+
type: proto.ProtocolErrorType.INTERNAL,
37+
message: errorMessage,
38+
});
39+
} else {
40+
error.id = messageId ?? errorId;
41+
process.stderr.write(
42+
`Host caused ${proto.ProtocolErrorType[error.type].toLowerCase()} error`
43+
);
44+
if (error.id !== errorId) process.stderr.write(` with request ${error.id}`);
45+
process.stderr.write(`: ${error.message}\n`);
46+
// PROTOCOL error from https://bit.ly/2poTt90
47+
process.exitCode = 76; // EX_PROTOCOL
48+
return error;
49+
}
50+
}

lib/src/embedded/worker_dispatcher.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright 2024 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {Observable} from 'rxjs';
6+
import {takeUntil} from 'rxjs/operators';
7+
import {create, fromBinary, toBinary} from '@bufbuild/protobuf';
8+
import * as varint from 'varint';
9+
10+
import * as pkg from '../../../package.json';
11+
import {PacketTransformer} from '../packet-transformer';
12+
import {ReusableWorker} from './reusable_worker';
13+
import {errorId, handleError, paramsError, parseError} from './utils';
14+
import * as proto from '../vendor/embedded_sass_pb';
15+
16+
export class WorkerDispatcher {
17+
private readonly allWorkers: ReusableWorker[] = [];
18+
19+
private readonly inactiveWorkers: ReusableWorker[] = [];
20+
21+
private readonly activeWorkers = new Map<number, ReusableWorker>();
22+
23+
private readonly stdin$ = new Observable<Buffer>(observer => {
24+
process.stdin.on('data', buffer => observer.next(buffer));
25+
}).pipe(
26+
takeUntil(
27+
new Promise(resolve => {
28+
process.stdin.on('close', () => resolve(undefined));
29+
})
30+
)
31+
);
32+
33+
private readonly packetTransformer = new PacketTransformer(
34+
this.stdin$,
35+
buffer => process.stdout.write(buffer)
36+
);
37+
38+
listen(): void {
39+
this.packetTransformer.protobufs$.subscribe({
40+
next: (buffer: Uint8Array) => {
41+
let compilationId: number;
42+
try {
43+
compilationId = varint.decode(buffer);
44+
} catch (error) {
45+
throw parseError(`Invalid compilation ID varint: ${error}`);
46+
}
47+
48+
try {
49+
if (compilationId !== 0) {
50+
if (this.activeWorkers.has(compilationId)) {
51+
const worker = this.activeWorkers.get(compilationId)!;
52+
worker.send(buffer);
53+
} else {
54+
const worker = this.getWorker(compilationId);
55+
this.activeWorkers.set(compilationId, worker);
56+
worker.send(buffer);
57+
}
58+
return;
59+
}
60+
61+
let message;
62+
try {
63+
message = fromBinary(
64+
proto.InboundMessageSchema,
65+
new Uint8Array(buffer.buffer, varint.decode.bytes)
66+
);
67+
} catch (error) {
68+
throw parseError(`Invalid protobuf: ${error}`);
69+
}
70+
71+
if (message.message.case !== 'versionRequest') {
72+
throw paramsError(
73+
`Only VersionRequest may have wire ID 0, was ${message.message.case}.`
74+
);
75+
}
76+
const request = message.message.value;
77+
const response = WorkerDispatcher.versionResponse();
78+
response.id = request.id;
79+
this.send(
80+
0,
81+
create(proto.OutboundMessageSchema, {
82+
message: {
83+
case: 'versionResponse',
84+
value: response,
85+
},
86+
})
87+
);
88+
} catch (error) {
89+
this.handleError(error);
90+
}
91+
},
92+
complete: () => {
93+
this.allWorkers.forEach(worker => worker.terminate());
94+
},
95+
error: error => {
96+
this.handleError(parseError(error.message));
97+
},
98+
});
99+
}
100+
101+
private getWorker(compilationId: number): ReusableWorker {
102+
let worker: ReusableWorker;
103+
if (this.inactiveWorkers.length > 0) {
104+
worker = this.inactiveWorkers.pop()!;
105+
} else {
106+
worker = new ReusableWorker(process.argv[1]);
107+
this.allWorkers.push(worker);
108+
}
109+
110+
worker.borrow(buffer => {
111+
const category = buffer.at(0);
112+
const packet = Buffer.from(buffer.buffer, 1);
113+
114+
switch (category) {
115+
case 0:
116+
this.packetTransformer.writeProtobuf(packet);
117+
break;
118+
case 1:
119+
this.activeWorkers.delete(compilationId);
120+
worker.release();
121+
this.inactiveWorkers.push(worker);
122+
this.packetTransformer.writeProtobuf(packet);
123+
break;
124+
case 2: {
125+
this.packetTransformer.writeProtobuf(packet);
126+
/* eslint-disable-next-line n/no-process-exit */
127+
process.exit();
128+
}
129+
}
130+
});
131+
132+
return worker;
133+
}
134+
135+
private handleError(
136+
error: Error | proto.ProtocolError,
137+
{
138+
compilationId,
139+
messageId,
140+
}: {compilationId?: number; messageId?: number} = {}
141+
): void {
142+
this.sendError(compilationId ?? errorId, handleError(error, {messageId}));
143+
process.stdin.destroy();
144+
}
145+
146+
private send(compilationId: number, message: proto.OutboundMessage): void {
147+
const compilationIdLength = varint.encodingLength(compilationId);
148+
const encodedMessage = toBinary(proto.OutboundMessageSchema, message);
149+
const buffer = new Uint8Array(compilationIdLength + encodedMessage.length);
150+
varint.encode(compilationId, buffer);
151+
buffer.set(encodedMessage, compilationIdLength);
152+
this.packetTransformer.writeProtobuf(buffer);
153+
}
154+
155+
private sendError(compilationId: number, error: proto.ProtocolError): void {
156+
this.send(
157+
compilationId,
158+
create(proto.OutboundMessageSchema, {
159+
message: {
160+
case: 'error',
161+
value: error,
162+
},
163+
})
164+
);
165+
}
166+
167+
static versionResponse(): proto.OutboundMessage_VersionResponse {
168+
return create(proto.OutboundMessage_VersionResponseSchema, {
169+
protocolVersion: pkg['protocol-version'],
170+
compilerVersion: pkg['compiler-version'],
171+
implementationVersion: pkg['version'],
172+
implementationName: 'dart-sass',
173+
});
174+
}
175+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy