Skip to content

Commit 96275cc

Browse files
committed
Implement sass --embedded in pure JS mode
1 parent 008bbef commit 96275cc

25 files changed

+528
-79
lines changed

bin/sass.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import 'package:sass/src/io.dart';
1818
import 'package:sass/src/stylesheet_graph.dart';
1919
import 'package:sass/src/utils.dart';
2020
import 'package:sass/src/embedded/executable.dart'
21-
// Never load the embedded protocol when compiling to JS.
22-
if (dart.library.js) 'package:sass/src/embedded/unavailable.dart'
21+
if (dart.library.js) 'package:sass/src/embedded/js/executable.dart'
2322
as embedded;
2423

2524
Future<void> main(List<String> args) async {

lib/src/embedded/compilation_dispatcher.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@
33
// https://opensource.org/licenses/MIT.
44

55
import 'dart:convert';
6-
import 'dart:io';
7-
import 'dart:isolate';
6+
import 'dart:isolate' if (dart.library.js) 'js/isolate.dart';
87
import 'dart:typed_data';
98

10-
import 'package:native_synchronization/mailbox.dart';
119
import 'package:path/path.dart' as p;
1210
import 'package:protobuf/protobuf.dart';
1311
import 'package:pub_semver/pub_semver.dart';
1412
import 'package:sass/sass.dart' as sass;
1513
import 'package:sass/src/importer/node_package.dart' as npi;
1614

15+
import '../io.dart' show FileSystemException;
1716
import '../logger.dart';
1817
import '../value/function.dart';
1918
import '../value/mixin.dart';
@@ -23,6 +22,7 @@ import 'host_callable.dart';
2322
import 'importer/file.dart';
2423
import 'importer/host.dart';
2524
import 'logger.dart';
25+
import 'sync_receive_port.dart';
2626
import 'util/proto_extensions.dart';
2727
import 'utils.dart';
2828

@@ -35,8 +35,8 @@ final _outboundRequestId = 0;
3535
/// A class that dispatches messages to and from the host for a single
3636
/// compilation.
3737
final class CompilationDispatcher {
38-
/// The mailbox for receiving messages from the host.
39-
final Mailbox _mailbox;
38+
/// The synchronous receive port for receiving messages from the host.
39+
final SyncReceivePort _receivePort;
4040

4141
/// The send port for sending messages to the host.
4242
final SendPort _sendPort;
@@ -52,8 +52,8 @@ final class CompilationDispatcher {
5252
late Uint8List _compilationIdVarint;
5353

5454
/// Creates a [CompilationDispatcher] that receives encoded protocol buffers
55-
/// through [_mailbox] and sends them through [_sendPort].
56-
CompilationDispatcher(this._mailbox, this._sendPort);
55+
/// through [_receivePort] and sends them through [_sendPort].
56+
CompilationDispatcher(this._receivePort, this._sendPort);
5757

5858
/// Listens for incoming `CompileRequests` and runs their compilations.
5959
void listen() {
@@ -427,9 +427,9 @@ final class CompilationDispatcher {
427427
/// Receive a packet from the host.
428428
Uint8List _receive() {
429429
try {
430-
return _mailbox.take();
430+
return _receivePort.receive();
431431
} on StateError catch (_) {
432-
// The [_mailbox] has been closed, exit the current isolate immediately
432+
// The [SyncReceivePort] has been closed, exit the current isolate immediately
433433
// to avoid bubble the error up as [SassException] during [_sendRequest].
434434
Isolate.exit();
435435
}

lib/src/embedded/concurrency.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2024 Google Inc. 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 'dart:ffi';
6+
7+
/// More than MaxMutatorThreadCount isolates in the same isolate group
8+
/// can deadlock the Dart VM.
9+
/// See https://github.com/sass/dart-sass/pull/2019
10+
int get concurrencyLimit => sizeOf<IntPtr>() <= 4 ? 7 : 15;

lib/src/embedded/executable.dart

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,19 @@
33
// https://opensource.org/licenses/MIT.
44

55
import 'dart:io';
6-
import 'dart:convert';
76

87
import 'package:stream_channel/stream_channel.dart';
98

109
import 'isolate_dispatcher.dart';
10+
import 'options.dart';
1111
import 'util/length_delimited_transformer.dart';
1212

1313
void main(List<String> args) {
14-
switch (args) {
15-
case ["--version", ...]:
16-
var response = IsolateDispatcher.versionResponse();
17-
response.id = 0;
18-
stdout.writeln(
19-
JsonEncoder.withIndent(" ").convert(response.toProto3Json()),
20-
);
21-
return;
22-
23-
case [_, ...]:
24-
stderr.writeln(
25-
"sass --embedded is not intended to be executed with additional "
26-
"arguments.\n"
27-
"See https://github.com/sass/dart-sass#embedded-dart-sass for "
28-
"details.",
29-
);
30-
// USAGE error from https://bit.ly/2poTt90
31-
exitCode = 64;
32-
return;
14+
if (parseOptions(args)) {
15+
IsolateDispatcher(
16+
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
17+
.transform(lengthDelimited),
18+
gracefulShutdown: false)
19+
.listen();
3320
}
34-
35-
IsolateDispatcher(
36-
StreamChannel.withGuarantees(
37-
stdin,
38-
stdout,
39-
allowSinkErrors: false,
40-
).transform(lengthDelimited),
41-
).listen();
4221
}

lib/src/embedded/isolate_dispatcher.dart

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@
33
// https://opensource.org/licenses/MIT.
44

55
import 'dart:async';
6-
import 'dart:ffi';
7-
import 'dart:io';
8-
import 'dart:isolate';
6+
import 'dart:io' if (dart.library.js) 'js/io.dart';
97
import 'dart:typed_data';
108

11-
import 'package:native_synchronization/mailbox.dart';
129
import 'package:pool/pool.dart';
1310
import 'package:protobuf/protobuf.dart';
1411
import 'package:stream_channel/stream_channel.dart';
1512

16-
import 'compilation_dispatcher.dart';
13+
import 'concurrency.dart' if (dart.library.js) 'js/concurrency.dart';
1714
import 'embedded_sass.pb.dart';
18-
import 'reusable_isolate.dart';
15+
import 'isolate_main.dart' if (dart.library.js) 'js/isolate_main.dart';
16+
import 'reusable_isolate.dart' if (dart.library.js) 'js/reusable_isolate.dart';
1917
import 'util/proto_extensions.dart';
2018
import 'utils.dart';
2119

@@ -25,6 +23,10 @@ class IsolateDispatcher {
2523
/// The channel of encoded protocol buffers, connected to the host.
2624
final StreamChannel<Uint8List> _channel;
2725

26+
/// Whether to wait for all worker isolates to exit before exiting the main
27+
/// isolate or not.
28+
final bool _gracefulShutdown;
29+
2830
/// All isolates that have been spawned to dispatch to.
2931
///
3032
/// Only used for cleaning up the process when the underlying channel closes.
@@ -38,16 +40,13 @@ class IsolateDispatcher {
3840

3941
/// A pool controlling how many isolates (and thus concurrent compilations)
4042
/// may be live at once.
41-
///
42-
/// More than MaxMutatorThreadCount isolates in the same isolate group
43-
/// can deadlock the Dart VM.
44-
/// See https://github.com/sass/dart-sass/pull/2019
45-
final _isolatePool = Pool(sizeOf<IntPtr>() <= 4 ? 7 : 15);
43+
final _isolatePool = Pool(concurrencyLimit);
4644

4745
/// Whether [_channel] has been closed or not.
4846
var _closed = false;
4947

50-
IsolateDispatcher(this._channel);
48+
IsolateDispatcher(this._channel, {bool gracefulShutdown = true})
49+
: _gracefulShutdown = gracefulShutdown;
5150

5251
void listen() {
5352
_channel.stream.listen(
@@ -107,8 +106,12 @@ class IsolateDispatcher {
107106
_handleError(error, stackTrace);
108107
},
109108
onDone: () {
110-
_closed = true;
111-
_allIsolates.stream.listen((isolate) => isolate.kill());
109+
if (_gracefulShutdown) {
110+
_closed = true;
111+
_allIsolates.stream.listen((isolate) => isolate.kill());
112+
} else {
113+
exit(exitCode);
114+
}
112115
},
113116
);
114117
}
@@ -125,7 +128,7 @@ class IsolateDispatcher {
125128
_inactiveIsolates.remove(isolate);
126129
} else {
127130
var future = ReusableIsolate.spawn(
128-
_isolateMain,
131+
isolateMain,
129132
onError: (Object error, StackTrace stackTrace) {
130133
_handleError(error, stackTrace);
131134
},
@@ -158,7 +161,11 @@ class IsolateDispatcher {
158161
_channel.sink.add(packet);
159162
case 2:
160163
_channel.sink.add(packet);
161-
exit(exitCode);
164+
if (_gracefulShutdown) {
165+
_channel.sink.close();
166+
} else {
167+
exit(exitCode);
168+
}
162169
}
163170
});
164171

@@ -188,7 +195,11 @@ class IsolateDispatcher {
188195
compilationId ?? errorId,
189196
handleError(error, stackTrace, messageId: messageId),
190197
);
191-
_channel.sink.close();
198+
if (_gracefulShutdown) {
199+
_channel.sink.close();
200+
} else {
201+
exit(exitCode);
202+
}
192203
}
193204

194205
/// Sends [message] to the host.
@@ -199,7 +210,3 @@ class IsolateDispatcher {
199210
void sendError(int compilationId, ProtocolError error) =>
200211
_send(compilationId, OutboundMessage()..error = error);
201212
}
202-
203-
void _isolateMain(Mailbox mailbox, SendPort sendPort) {
204-
CompilationDispatcher(mailbox, sendPort).listen();
205-
}

lib/src/embedded/isolate_main.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2024 Google Inc. 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 'dart:isolate' show SendPort;
6+
7+
import 'compilation_dispatcher.dart';
8+
import 'sync_receive_port.dart';
9+
10+
void isolateMain(SyncReceivePort receivePort, SendPort sendPort) {
11+
CompilationDispatcher(receivePort, sendPort).listen();
12+
}

lib/src/embedded/js/concurrency.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2024 Google Inc. 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 'dart:js_interop';
6+
7+
8+
@JS('os.cpus')
9+
external JSArray _cpus();
10+
11+
int get concurrencyLimit => _cpus().length;

lib/src/embedded/js/executable.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2024 Google Inc. 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 'package:stream_channel/stream_channel.dart';
6+
7+
import '../isolate_dispatcher.dart';
8+
import '../isolate_main.dart';
9+
import '../options.dart';
10+
import '../util/length_delimited_transformer.dart';
11+
import 'io.dart';
12+
import 'sync_receive_port.dart';
13+
import 'worker_threads.dart';
14+
15+
void main(List<String> args) {
16+
if (parseOptions(args)) {
17+
if (isMainThread) {
18+
IsolateDispatcher(StreamChannel.withGuarantees(stdin, stdout,
19+
allowSinkErrors: false)
20+
.transform(lengthDelimited))
21+
.listen();
22+
} else {
23+
var port = workerData! as MessagePort;
24+
isolateMain(JSSyncReceivePort(port), JSSendPort(port));
25+
}
26+
}
27+
}

lib/src/embedded/js/io.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2024 Google Inc. 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 'dart:async';
6+
import 'dart:js_interop';
7+
import 'dart:typed_data';
8+
9+
@JS('process.exitCode')
10+
external int? get _exitCode;
11+
int get exitCode => _exitCode ?? 0;
12+
13+
@JS('process.exitCode')
14+
external set exitCode(int code);
15+
16+
@JS('process.exit')
17+
external void exit([int code]);
18+
19+
@JS()
20+
extension type _ReadStream(JSObject _) implements JSObject {
21+
external void destroy();
22+
external void on(String type, JSFunction listener);
23+
}
24+
25+
@JS('process.stdin')
26+
external _ReadStream get _stdin;
27+
28+
@JS()
29+
extension type _WriteStream(JSObject _) implements JSObject {
30+
external void write(JSUint8Array chunk);
31+
}
32+
33+
@JS('process.stdout')
34+
external _WriteStream get _stdout;
35+
36+
Stream<List<int>> get stdin {
37+
var controller = StreamController<Uint8List>(
38+
onCancel: () {
39+
_stdin.destroy();
40+
},
41+
sync: true);
42+
_stdin.on(
43+
'data',
44+
(JSUint8Array chunk) {
45+
controller.sink.add(chunk.toDart);
46+
}.toJS);
47+
_stdin.on(
48+
'end',
49+
() {
50+
controller.sink.close();
51+
}.toJS);
52+
_stdin.on(
53+
'error',
54+
(JSObject e) {
55+
controller.sink.addError(e);
56+
}.toJS);
57+
return controller.stream;
58+
}
59+
60+
StreamSink<List<int>> get stdout {
61+
var controller = StreamController<Uint8List>(sync: true);
62+
controller.stream.listen((buffer) {
63+
_stdout.write(buffer.toJS);
64+
});
65+
return controller.sink;
66+
}

lib/src/embedded/js/isolate.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 'dart:isolate' show SendPort;
6+
export 'dart:isolate' show SendPort;
7+
8+
import 'io.dart' as io;
9+
10+
abstract class Isolate {
11+
static Never exit([SendPort? finalMessagePort, Object? message]) {
12+
if (message != null) {
13+
finalMessagePort?.send(message);
14+
}
15+
io.exit(io.exitCode) as Never;
16+
}
17+
}

0 commit comments

Comments
 (0)