Skip to content

Commit 54fabf3

Browse files
committed
Implement sass --embedded in pure JS mode
1 parent 7129352 commit 54fabf3

27 files changed

+484
-77
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';
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() {
@@ -384,9 +384,9 @@ final class CompilationDispatcher {
384384
/// Receive a packet from the host.
385385
Uint8List _receive() {
386386
try {
387-
return _mailbox.take();
387+
return _receivePort.receive();
388388
} on StateError catch (_) {
389-
// The [_mailbox] has been closed, exit the current isolate immediately
389+
// The [SyncReceivePort] has been closed, exit the current isolate immediately
390390
// to avoid bubble the error up as [SassException] during [_sendRequest].
391391
Isolate.exit();
392392
}

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 & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +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-
return;
21-
22-
case [_, ...]:
23-
stderr.writeln(
24-
"sass --embedded is not intended to be executed with additional "
25-
"arguments.\n"
26-
"See https://github.com/sass/dart-sass#embedded-dart-sass for "
27-
"details.");
28-
// USAGE error from https://bit.ly/2poTt90
29-
exitCode = 64;
30-
return;
14+
if (parseOptions(args)) {
15+
IsolateDispatcher(
16+
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
17+
.transform(lengthDelimited), () {
18+
exit(exitCode);
19+
}).listen();
3120
}
32-
33-
IsolateDispatcher(
34-
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
35-
.transform(lengthDelimited))
36-
.listen();
3721
}

lib/src/embedded/isolate_dispatcher.dart

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

55
import 'dart:async';
6-
import 'dart:ffi';
7-
import 'dart:io';
8-
import 'dart:isolate';
96
import 'dart:typed_data';
107

11-
import 'package:native_synchronization/mailbox.dart';
128
import 'package:pool/pool.dart';
139
import 'package:protobuf/protobuf.dart';
1410
import 'package:stream_channel/stream_channel.dart';
1511

16-
import 'compilation_dispatcher.dart';
12+
import 'concurrency.dart' if (dart.library.js) 'js/concurrency.dart';
1713
import 'embedded_sass.pb.dart';
18-
import 'reusable_isolate.dart';
14+
import 'isolate_main.dart';
15+
import 'reusable_isolate.dart' if (dart.library.js) 'js/reusable_isolate.dart';
1916
import 'util/proto_extensions.dart';
2017
import 'utils.dart';
2118

@@ -25,6 +22,9 @@ class IsolateDispatcher {
2522
/// The channel of encoded protocol buffers, connected to the host.
2623
final StreamChannel<Uint8List> _channel;
2724

25+
/// The callback which is called when fatal error occurs.
26+
final void Function() _onError;
27+
2828
/// All isolates that have been spawned to dispatch to.
2929
///
3030
/// Only used for cleaning up the process when the underlying channel closes.
@@ -38,16 +38,12 @@ class IsolateDispatcher {
3838

3939
/// A pool controlling how many isolates (and thus concurrent compilations)
4040
/// 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);
41+
final _isolatePool = Pool(concurrencyLimit);
4642

4743
/// Whether [_channel] has been closed or not.
4844
var _closed = false;
4945

50-
IsolateDispatcher(this._channel);
46+
IsolateDispatcher(this._channel, this._onError);
5147

5248
void listen() {
5349
_channel.stream.listen((packet) async {
@@ -112,7 +108,7 @@ class IsolateDispatcher {
112108
isolate = _inactiveIsolates.first;
113109
_inactiveIsolates.remove(isolate);
114110
} else {
115-
var future = ReusableIsolate.spawn(_isolateMain,
111+
var future = ReusableIsolate.spawn(isolateMain,
116112
onError: (Object error, StackTrace stackTrace) {
117113
_handleError(error, stackTrace);
118114
});
@@ -144,7 +140,7 @@ class IsolateDispatcher {
144140
_channel.sink.add(packet);
145141
case 2:
146142
_channel.sink.add(packet);
147-
exit(exitCode);
143+
_onError();
148144
}
149145
});
150146

@@ -168,7 +164,7 @@ class IsolateDispatcher {
168164
{int? compilationId, int? messageId}) {
169165
sendError(compilationId ?? errorId,
170166
handleError(error, stackTrace, messageId: messageId));
171-
_channel.sink.close();
167+
_onError();
172168
}
173169

174170
/// Sends [message] to the host.
@@ -179,7 +175,3 @@ class IsolateDispatcher {
179175
void sendError(int compilationId, ProtocolError error) =>
180176
_send(compilationId, OutboundMessage()..error = error);
181177
}
182-
183-
void _isolateMain(Mailbox mailbox, SendPort sendPort) {
184-
CompilationDispatcher(mailbox, sendPort).listen();
185-
}

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: 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:js_interop';
6+
7+
@JS('os.cpus')
8+
external JSArray _cpus();
9+
10+
int get concurrencyLimit => _cpus().toDart.length;

lib/src/embedded/js/executable.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 '../compilation_dispatcher.dart';
8+
import '../isolate_dispatcher.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(
19+
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
20+
.transform(lengthDelimited), () {
21+
stdinDestory();
22+
}).listen();
23+
} else {
24+
var port = workerData! as MessagePort;
25+
CompilationDispatcher(JSSyncReceivePort(port), JSSendPort(port)).listen();
26+
}
27+
}
28+
}

lib/src/embedded/js/io.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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:typed_data';
7+
import 'dart:js_interop';
8+
9+
@JS('process.stdin.destroy')
10+
external void stdinDestory();
11+
12+
@JS('process.stdin.on')
13+
external void _stdinOn(String type, JSFunction listener);
14+
15+
@JS('process.stdout.write')
16+
external void _stdoutWrite(JSUint8Array buffer);
17+
18+
Stream<List<int>> get stdin {
19+
var controller = StreamController<Uint8List>(
20+
onCancel: () {
21+
stdinDestory();
22+
},
23+
sync: true);
24+
_stdinOn(
25+
'data',
26+
(JSUint8Array chunk) {
27+
controller.sink.add(chunk.toDart);
28+
}.toJS);
29+
_stdinOn(
30+
'end',
31+
() {
32+
controller.sink.close();
33+
}.toJS);
34+
_stdinOn(
35+
'error',
36+
(JSObject e) {
37+
controller.sink.addError(e);
38+
}.toJS);
39+
return controller.stream;
40+
}
41+
42+
StreamSink<List<int>> get stdout {
43+
var controller = StreamController<Uint8List>(sync: true);
44+
controller.stream.listen((buffer) {
45+
_stdoutWrite(buffer.toJS);
46+
});
47+
return controller.sink;
48+
}

lib/src/embedded/js/isolate.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
import 'dart:js_interop';
8+
9+
@JS('process.exit')
10+
external void _exit();
11+
12+
abstract class Isolate {
13+
static Never exit([SendPort? finalMessagePort, Object? message]) {
14+
if (message != null) {
15+
finalMessagePort?.send(message);
16+
}
17+
_exit();
18+
19+
// This is unreachable, but needed for return type [Never]
20+
throw Error();
21+
}
22+
}

0 commit comments

Comments
 (0)