Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/implement-hmr #141

Merged
merged 14 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/application/environment/app_env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ enum AppEnv implements EnvSchema {
httpVersion('HTTP_VERSION'),
wssVersion('WSS_VERSION'),
intent('INTENT'),
logLevel('LOG_LEVEL');
logLevel('LOG_LEVEL'),
hmr('HMR');

@override
final String key;
Expand Down
3 changes: 3 additions & 0 deletions lib/application/environment/environment.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:io';

import 'package:mineral/application/container/ioc_container.dart';
import 'package:mineral/application/environment/env_schema.dart';

abstract interface class EnvContract {
Expand Down Expand Up @@ -89,4 +90,6 @@ final class Environment implements EnvContract {

return files;
}

factory Environment.singleton() => ioc.resolve('environment');
}
38 changes: 38 additions & 0 deletions lib/application/hmr/entities/directory_watcher_element.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:io';
import 'package:mineral/application/hmr/entities/watcher_element.dart';
import 'package:watcher/watcher.dart';

final class DirectoryWatcherElement implements WatcherElement {
final Directory appRoot;
final Directory watcherRoot;
late final DirectoryWatcher _watcher;

final void Function(WatchEvent event) addFile;
final void Function(WatchEvent event) editFile;
final void Function(WatchEvent event) removeFile;

DirectoryWatcherElement({
required this.appRoot,
required this.watcherRoot,
required this.addFile,
required this.editFile,
required this.removeFile
}) {
_watcher = DirectoryWatcher(watcherRoot.path);
}

@override
void watch() {
_watcher.events.listen(dispatch);
}

@override
void dispatch (WatchEvent event) {
return switch (event.type) {
ChangeType.ADD => addFile(event),
ChangeType.MODIFY => editFile(event),
ChangeType.REMOVE => removeFile(event),
_ => null
};
}
}
38 changes: 38 additions & 0 deletions lib/application/hmr/entities/file_watcher_element.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:io';
import 'package:mineral/application/hmr/entities/watcher_element.dart';
import 'package:watcher/watcher.dart';

final class FileWatcherElement implements WatcherElement {
final Directory appRoot;
final File watchedFile;
late final FileWatcher _watcher;

final void Function(WatchEvent event) addFile;
final void Function(WatchEvent event) editFile;
final void Function(WatchEvent event) removeFile;

FileWatcherElement({
required this.appRoot,
required this.watchedFile,
required this.addFile,
required this.editFile,
required this.removeFile
}) {
_watcher = FileWatcher(watchedFile.path);
}

@override
void watch() {
_watcher.events.listen(dispatch);
}

@override
void dispatch (WatchEvent event) {
return switch (event.type) {
ChangeType.ADD => addFile(event),
ChangeType.MODIFY => editFile(event),
ChangeType.REMOVE => removeFile(event),
_ => null
};
}
}
6 changes: 6 additions & 0 deletions lib/application/hmr/entities/watcher_element.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:watcher/watcher.dart';

abstract interface class WatcherElement {
void watch();
void dispatch (WatchEvent event);
}
132 changes: 132 additions & 0 deletions lib/application/hmr/hot_module_reloading.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:isolate';

import 'package:mineral/application/hmr/watcher_builder.dart';
import 'package:mineral/application/hmr/watcher_config.dart';
import 'package:mineral/application/io/ansi.dart';
import 'package:mineral/domains/data/data_listener.dart';
import 'package:mineral/domains/data_store/data_store.dart';
import 'package:mineral/domains/wss/shard.dart';
import 'package:mineral/domains/wss/shard_message.dart';
import 'package:path/path.dart';
import 'package:watcher/watcher.dart';

final class HotModuleReloading {
final WatcherConfig _watcherConfig;
final SendPort? _devPort;

String fileLocation = '';
int fileRefreshCount = 0;

Isolate? _devIsolate;
SendPort? devSendPort;
DateTime? duration;

final DataStoreContract _datastore;
final DataListenerContract _dataListener;
final Map<int, Shard> _shards;
final Function() _createShards;

HotModuleReloading(this._devPort, this._watcherConfig, this._datastore, this._dataListener,
this._createShards, this._shards);

Future<void> spawn() async {
if (Isolate.current.debugName == 'dev') {
final ReceivePort port = ReceivePort();
final Stream stream = port.asBroadcastStream();

_devPort!.send(port.sendPort);
await _datastore.marshaller.cache.init();
await for (final Map<String, dynamic> message in stream) {
_dataListener.packets.dispatch(ShardMessageImpl.of(message));
}
} else {
_createHotModuleLoader();
_createDevelopmentIsolate();
_createShards();
}
}

void _createHotModuleLoader() {
final watcher = WatcherBuilder(Directory.current)
.setAllowReload(true)
.addWatchFolder(Directory(join(Directory.current.path, 'lib')));

for (final file in _watcherConfig.watchedFiles) {
watcher.addWatchFile(file);
}

for (final folder in _watcherConfig.watchedFolders) {
watcher.addWatchFolder(folder);
}

watcher.onReload(_handleModify).build().watch();
}

void _createDevelopmentIsolate() {
final port = ReceivePort();
final uri = Uri.parse(join(Directory.current.path, 'lib', 'main.dart'));

Isolate.spawnUri(Uri.file(uri.path), [], port.sendPort, debugName: 'dev')
.then((Isolate isolate) async {
_devIsolate = isolate;
devSendPort = await port.first;

_shards.forEach((key, value) {
final Queue<Map<String, dynamic>> queue = Queue.from(value.onceEventQueue);
while (queue.isNotEmpty) {
final response = queue.removeFirst();
devSendPort?.send(response);
}
});
});
}

void _handleModify(WatchEvent event) {
if (Platform.isLinux && duration != null) {
if (DateTime.now().difference(duration!) < Duration(milliseconds: 5)) {
return;
}
}

final String location = event.path.replaceFirst(Directory.current.path, '').substring(1);
final now = DateTime.now();
final time = '${now.hour}:${now.minute}:${now.second}';

if (fileLocation == location) {
fileRefreshCount++;
} else {
fileLocation = location;
fileRefreshCount = 1;
}

String formatMessage(String action) =>
'$time ${lightBlue.wrap('[mineral]')} ${lightGreen.wrap('hmr $action')} ${styleDim.wrap(location)} ${yellow.wrap('(x$fileRefreshCount)')}';

stdout
..write('\x1b[0;0H')
..write('\x1b[2J');

final message = switch (event.type) {
ChangeType.ADD => formatMessage('create'),
ChangeType.MODIFY => formatMessage('update'),
ChangeType.REMOVE => formatMessage('delete'),
_ => '',
};

stdout
..writeln(message)
..writeln();

_devIsolate?.kill(priority: Isolate.immediate);
_devIsolate = null;

_createDevelopmentIsolate();

if (Platform.isLinux) {
duration = DateTime.now();
}
}
}
42 changes: 42 additions & 0 deletions lib/application/hmr/watcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'dart:io';

import 'package:mineral/application/hmr/entities/directory_watcher_element.dart';
import 'package:mineral/application/hmr/entities/file_watcher_element.dart';
import 'package:mineral/application/hmr/entities/watcher_element.dart';
import 'package:watcher/watcher.dart';

final class Watcher {
final Directory appRoot;
late final List<WatcherElement> watchers = [];
final bool allowReload;
void Function(WatchEvent event) onReload;

Watcher({ required this.allowReload, required this.appRoot, required List<Directory> folders, required List<File> files, required this.onReload }) {
watchers.addAll(List.from([
...folders.map((folder) =>
DirectoryWatcherElement(
appRoot: appRoot,
watcherRoot: folder,
addFile: onReload,
editFile: onReload,
removeFile: onReload
)
),
...files.map((file) =>
FileWatcherElement(
appRoot: appRoot,
watchedFile: file,
addFile: onReload,
editFile: onReload,
removeFile: onReload
)
)
]));
}

void watch() {
for (final watcher in watchers) {
watcher.watch();
}
}
}
43 changes: 43 additions & 0 deletions lib/application/hmr/watcher_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:io';

import 'package:mineral/application/hmr/watcher.dart';
import 'package:watcher/watcher.dart' as watcher;

final class WatcherBuilder {
final Directory _appRoot;
final List<Directory> _folderWatchers = [];
final List<File> _fileWatchers = [];

bool _allowReload = false;
void Function(watcher.WatchEvent) _onReload = (_) {};

WatcherBuilder(this._appRoot);

WatcherBuilder setAllowReload (bool value) {
_allowReload = value;
return this;
}

WatcherBuilder addWatchFolder (Directory value) {
_folderWatchers.add(value);
return this;
}

WatcherBuilder addWatchFile (File value) {
_fileWatchers.add(value);
return this;
}

WatcherBuilder onReload (Function(watcher.WatchEvent) callback) {
_onReload = callback;
return this;
}

Watcher build () => Watcher(
allowReload: _allowReload,
appRoot: _appRoot,
folders: _folderWatchers,
files: _fileWatchers,
onReload: _onReload
);
}
6 changes: 6 additions & 0 deletions lib/application/hmr/watcher_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'dart:io';

final class WatcherConfig {
final List<Directory> watchedFolders = [];
final List<File> watchedFiles = [];
}
56 changes: 56 additions & 0 deletions lib/application/io/ansi.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export 'package:io/ansi.dart'
show
AnsiCode,
AnsiCodeType,
ansiOutputEnabled,
backgroundBlack,
backgroundBlue,
backgroundColors,
backgroundCyan,
backgroundDarkGray,
backgroundDefault,
backgroundGreen,
backgroundLightBlue,
backgroundLightCyan,
backgroundLightGray,
backgroundLightGreen,
backgroundLightMagenta,
backgroundLightRed,
backgroundLightYellow,
backgroundMagenta,
backgroundRed,
backgroundWhite,
backgroundYellow,
black,
blue,
cyan,
darkGray,
defaultForeground,
foregroundColors,
green,
lightBlue,
lightCyan,
lightGray,
lightGreen,
lightMagenta,
lightRed,
lightYellow,
magenta,
overrideAnsiOutput,
red,
resetAll,
resetBlink,
resetBold,
resetDim,
resetItalic,
resetReverse,
resetUnderlined,
styleBlink,
styleBold,
styleDim,
styleItalic,
styleReverse,
styleUnderlined,
styles,
white,
yellow;
Loading