Skip to content

Commit d7e6206

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

27 files changed

+490
-74
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: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,18 @@
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+
.listen();
3119
}
32-
33-
IsolateDispatcher(
34-
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
35-
.transform(lengthDelimited))
36-
.listen();
3720
}

lib/src/embedded/isolate_dispatcher.dart

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@
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 '../io.dart';
14+
import 'concurrency.dart' if (dart.library.js) 'js/concurrency.dart';
1715
import 'embedded_sass.pb.dart';
18-
import 'reusable_isolate.dart';
16+
import 'isolate_main.dart';
17+
import 'reusable_isolate.dart' if (dart.library.js) 'js/reusable_isolate.dart';
1918
import 'util/proto_extensions.dart';
2019
import 'utils.dart';
2120

@@ -38,11 +37,7 @@ class IsolateDispatcher {
3837

3938
/// A pool controlling how many isolates (and thus concurrent compilations)
4039
/// 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);
40+
final _isolatePool = Pool(concurrencyLimit);
4641

4742
/// Whether [_channel] has been closed or not.
4843
var _closed = false;
@@ -112,7 +107,7 @@ class IsolateDispatcher {
112107
isolate = _inactiveIsolates.first;
113108
_inactiveIsolates.remove(isolate);
114109
} else {
115-
var future = ReusableIsolate.spawn(_isolateMain,
110+
var future = ReusableIsolate.spawn(isolateMain,
116111
onError: (Object error, StackTrace stackTrace) {
117112
_handleError(error, stackTrace);
118113
});
@@ -179,7 +174,3 @@ class IsolateDispatcher {
179174
void sendError(int compilationId, ProtocolError error) =>
180175
_send(compilationId, OutboundMessage()..error = error);
181176
}
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: 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 '../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(StreamChannel.withGuarantees(stdin, stdout,
19+
allowSinkErrors: false)
20+
.transform(lengthDelimited))
21+
.listen();
22+
} else {
23+
var port = workerData! as MessagePort;
24+
CompilationDispatcher(JSSyncReceivePort(port), JSSendPort(port)).listen();
25+
}
26+
}
27+
}

lib/src/embedded/js/io.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
import '../../io.dart';
10+
11+
/// In Node JS's main thread we need to wait for worker to exit in order
12+
/// to get exit code set by worker asynchronously. Therefore skip explict
13+
/// exit on main thread, but close stdin and wait for shutdown.
14+
void exit([int? code]) {
15+
if (code != null) {
16+
exitCode = code;
17+
}
18+
_stdinDestory();
19+
}
20+
21+
@JS('process.stdin.destroy')
22+
external void _stdinDestory();
23+
24+
@JS('process.stdin.on')
25+
external void _stdinOn(String type, JSFunction listener);
26+
27+
@JS('process.stdout.write')
28+
external void _stdoutWrite(JSUint8Array buffer);
29+
30+
Stream<List<int>> get stdin {
31+
var controller = StreamController<Uint8List>(
32+
onCancel: () {
33+
_stdinDestory();
34+
},
35+
sync: true);
36+
_stdinOn(
37+
'data',
38+
(JSUint8Array chunk) {
39+
controller.sink.add(chunk.toDart);
40+
}.toJS);
41+
_stdinOn(
42+
'end',
43+
() {
44+
controller.sink.close();
45+
}.toJS);
46+
_stdinOn(
47+
'error',
48+
(JSObject e) {
49+
controller.sink.addError(e);
50+
}.toJS);
51+
return controller.stream;
52+
}
53+
54+
StreamSink<List<int>> get stdout {
55+
var controller = StreamController<Uint8List>(sync: true);
56+
controller.stream.listen((buffer) {
57+
_stdoutWrite(buffer.toJS);
58+
});
59+
return controller.sink;
60+
}

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)