diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e644f5a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Jaime Blasco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/flutter-package/LICENSE b/flutter-package/LICENSE index ba75c69..e644f5a 100644 --- a/flutter-package/LICENSE +++ b/flutter-package/LICENSE @@ -1 +1,21 @@ -TODO: Add your license here. +MIT License + +Copyright (c) 2020 Jaime Blasco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/flutter-package/bin/daemon_service.dart b/flutter-package/bin/daemon_service.dart new file mode 100644 index 0000000..e420ffa --- /dev/null +++ b/flutter-package/bin/daemon_service.dart @@ -0,0 +1,136 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_rpc_2/json_rpc_2.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import 'preview.dart'; + +class DaemonService extends MultiplePeer { + final int port; + DaemonService(this.port); + + @override + registerMethods(Peer server) { + server.registerMethod('preview.getPort', () { + return port; + }); + + server.registerMethod('preview.setActiveFile', (Parameters params) { + final path = params['path'].asString; + changeActiveFile(path); + return true; + }); + + server.registerMethod('preview.restart', (Parameters params) { + _server.sendNotification('preview.restart'); + return true; + }); + } +} + +class Stoutsink extends StreamSink { + @override + void add(String event) { + stdout.writeln(event); + } + + @override + void addError(Object error, [StackTrace stackTrace]) { + stdout.addError(error, stackTrace); + } + + @override + Future addStream(Stream stream) { + return stdout.addStream(stream.transform(Utf8Encoder())); + } + + @override + Future close() { + return stdout.close(); + } + + @override + Future get done => stdout.close(); +} + +abstract class MultiplePeer { + Peer _server; + Map sockets = {}; + + //StreamChannel _socket; + + bool isListening = false; + + addWebSocket(WebSocketChannel webSocket) { + assert(sockets[webSocket] == null); + final peer = Peer(webSocket.cast(), strictProtocolChecks: false); + sockets[webSocket] = peer; + registerMethods(peer); + if (isListening) { + peer.listen(); + } + } + + removeWebSocket(WebSocketChannel webSocket) { + final peer = sockets[webSocket]; + peer.close(); + sockets[webSocket] = null; + } + + Future run() async { + // assert(port != null && webSocket != null); + try { + /* _socket = webSocket ?? + WebSocketChannel.connect(Uri.parse('ws://127.0.0.1:$port/ws')) + .cast(); */ + } catch (e, s) { + stderr.addError(e, s); + } + + final channel = StreamChannel( + stdin.transform(Utf8Decoder()).transform(LineSplitter()), + Stoutsink(), + ); + + _server = Peer(channel, strictProtocolChecks: false, + onUnhandledError: (error, stacktrace) { + stdout.write('Error $error'); + }); + + registerMethods(_server); + } + + Future listen() async { + isListening = true; + for (final socket in sockets.entries) { + socket.value.listen().then( + (value) { + removeWebSocket(socket.key); + }, + ); + } + return await _server.listen(); + } + + Future close() async { + for (final peer in sockets.values) { + await peer.close(); + } + return _server.close(); + } + + /* void registerMethod(String name, Function callback) => + _server.registerMethod(name, callback); */ + + void sendNotification(String method, [dynamic parameters]) { + _server.sendNotification(method, parameters); + sockets.values.forEach((e) { + e.sendNotification(method, parameters); + }); + } + + registerMethods(Peer server); +} diff --git a/flutter-package/bin/old/asset_server.dart b/flutter-package/bin/old/asset_server.dart new file mode 100644 index 0000000..5cf8c9b --- /dev/null +++ b/flutter-package/bin/old/asset_server.dart @@ -0,0 +1,56 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; + +final localAddress = InternetAddress('127.0.0.1'); + +class AssetServer { + HttpServer _server; + + Future runServer(int port) async { + try { + _server = await HttpServer.bind(localAddress, port); + return true; + } catch (e, s) { + stderr.addError(e, s); + return false; + } + } + + Future listen() async { + await for (final request in _server) { + try { + if (request.uri.path.startsWith('/asset/')) { + final File file = + new File(request.uri.path.replaceFirst('/asset/', '')); + + if (file.existsSync()) { + final raw = await file.readAsBytes(); + + request.response.headers + .set('Content-Type', 'image/${path.extension(file.path)}'); + request.response.headers.set('Content-Length', raw.length); + request.response.add(raw); + await request.response.close(); + } else { + request.response.statusCode = HttpStatus.notFound; + request.response.write('Not found'); + await request.response.close(); + } + } else { + request.response.statusCode = HttpStatus.notFound; + request.response.write('Not found'); + await request.response.close(); + } + } catch (e, s) { + stderr.addError(e, s); + request.response.statusCode = HttpStatus.internalServerError; + request.response.write('Internal Error'); + await request.response.close(); + } + } + } + + Future close() async { + await _server.close(); + } +} diff --git a/flutter-package/bin/old/daemon_service.dart b/flutter-package/bin/old/daemon_service.dart new file mode 100644 index 0000000..7cf923a --- /dev/null +++ b/flutter-package/bin/old/daemon_service.dart @@ -0,0 +1,188 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:json_rpc_2/json_rpc_2.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../preview.dart'; + +class Stoutsink extends StreamSink { + @override + void add(String event) { + stdout.writeln(event); + } + + @override + void addError(Object error, [StackTrace stackTrace]) { + stdout.addError(error, stackTrace); + } + + @override + Future addStream(Stream stream) { + return stdout.addStream(stream.transform(Utf8Encoder())); + } + + @override + Future close() { + return stdout.close(); + } + + @override + Future get done => stdout.close(); +} + +class MultipleSink extends StreamSink { + final Iterable> _sinks; + + MultipleSink([this._sinks = const []]); + + @override + void add(T event) { + _sinks.forEach((sink) => sink.add(event)); + } + + @override + void addError(Object error, [StackTrace stackTrace]) { + _sinks.forEach((sink) => sink.addError(error, stackTrace)); + } + + @override + Future addStream(Stream stream) async { + for (final sink in _sinks) { + await sink.addStream(stream); + } + } + + @override + Future close() async { + for (final sink in _sinks) { + await sink.close(); + } + } + + @override + Future get done => Future.wait(_sinks.map((e) => e.done)); +} + +class DaemonService extends MultiplePeer { + final int port; + DaemonService(this.port); + + @override + registerMethods(Peer server) { + server.registerMethod('preview.getPort', () { + return port; + }); + + server.registerMethod('preview.setActiveFile', (Parameters params) { + final path = params['path'].asString; + changeActiveFile(path); + return true; + }); + + server.registerMethod('preview.restart', (Parameters params) { + _server.sendNotification('preview.restart'); + return true; + }); + } +} + +abstract class MultiplePeer { + Peer _server; + Map sockets = {}; + + //StreamChannel _socket; + + bool isListening = false; + + addWebSocket(WebSocketChannel webSocket) { + assert(sockets[webSocket] == null); + final peer = Peer(webSocket.cast(), strictProtocolChecks: false); + sockets[webSocket] = peer; + registerMethods(peer); + if (isListening) { + peer.listen(); + } + } + + removeWebSocket(WebSocketChannel webSocket) { + final peer = sockets[webSocket]; + peer.close(); + sockets[webSocket] = null; + } + + Future run() async { + // assert(port != null && webSocket != null); + try { + /* _socket = webSocket ?? + WebSocketChannel.connect(Uri.parse('ws://127.0.0.1:$port/ws')) + .cast(); */ + } catch (e, s) { + stderr.addError(e, s); + } + + final stream = StreamGroup.merge([ + // _socket.stream, + stdin.transform(Utf8Decoder()).transform(LineSplitter()) + ]); + + final channel = StreamChannel( + stream, + MultipleSink([ + // _socket.sink, + Stoutsink(), + ]), + ); + + _server = Peer(channel, strictProtocolChecks: false, + onUnhandledError: (error, stacktrace) { + stdout.write('Error $error'); + }); + + // Any string may be used as a method name. JSON-RPC 2.0 methods are + // case-sensitive. + var i = 0; + _server.registerMethod('count', () { + // Just return the value to be sent as a response to the client. This can + // be anything JSON-serializable, or a Future that completes to something + // JSON-serializable. + return i++; + }); + + registerMethods(_server); + } + + Future listen() async { + isListening = true; + for (final socket in sockets.entries) { + socket.value.listen().then( + (value) { + removeWebSocket(socket.key); + }, + ); + } + return await _server.listen(); + } + + Future close() async { + for (final peer in sockets.values) { + await peer.close(); + } + return _server.close(); + } + + /* void registerMethod(String name, Function callback) => + _server.registerMethod(name, callback); */ + + void sendNotification(String method, [dynamic parameters]) { + _server.sendNotification(method, parameters); + sockets.values.forEach((e) { + e.sendNotification(method, parameters); + }); + } + + registerMethods(Peer server); +} diff --git a/flutter-package/bin/hot_restart_server.dart b/flutter-package/bin/old/hot_restart_server.dart similarity index 100% rename from flutter-package/bin/hot_restart_server.dart rename to flutter-package/bin/old/hot_restart_server.dart diff --git a/flutter-package/bin/old/preview_daemon.dart b/flutter-package/bin/old/preview_daemon.dart new file mode 100644 index 0000000..20db5e0 --- /dev/null +++ b/flutter-package/bin/old/preview_daemon.dart @@ -0,0 +1,234 @@ +import 'dart:convert'; +import 'package:path/path.dart' as path; + +import 'dart:io'; + +import 'package:async/async.dart'; + +import '../utils.dart'; +import 'run.dart'; + +class PreviewDaemonService { + Stream _requests; + HttpServer _server; + + Future start() async { + final stinStream = getStindRequests(); + final succedd = await runServer(); + Stream requests; + if (succedd) { + requests = _server.map((event) => PreviewRequest(request: event)); + } + + _requests = StreamGroup.merge([ + stinStream, + if (requests != null) requests, + ]); + } + + Stream getStindRequests() { + return stdin + .transform(Utf8Decoder()) + .transform(LineSplitter()) + .map((event) => StdinRequest(event)); + } + + Future runServer() async { + try { + final address = InternetAddress('127.0.0.1'); + final port = await getUnusedPort(address); + print('Preview running on http://127.0.0.1:$port'); + _server = await HttpServer.bind(address, port); + return true; + } catch (e) { + stdout.write('error'); + stdout.write(e); + return false; + } + } + + Future listen() async { + await for (final request in _requests) { + final methods = request.parse(); + for (final method in methods) { + try { + switch (method.name) { + case 'preview.changeActiveFile': + handleOnActiveFileChanged(method, request); + break; + default: + stdout.write( + '[{"event": "preview.error", "result": {"message": "Method $method not found"}}]\n'); + } + } catch (e) { + stdout.write( + '[{"event": "preview.error", "result": {"message": "Invalid request: $e"}}]\n'); + } + } + + request.when( + stdin: (file) { + try { + if (File(file).existsSync()) { + generatePreview(file); + //request.response.write('Hello, world'); + } + } catch (e) { + stdout.write('error'); + stdout.write(e); + print(e); + } + }, + request: (request) async { + if (request.uri.path.startsWith('/asset/')) { + final File file = + new File(request.uri.path.replaceFirst('/asset/', '')); + + if (file.existsSync()) { + final raw = await file.readAsBytes(); + + request.response.headers + .set('Content-Type', 'image/${path.extension(file.path)}'); + request.response.headers.set('Content-Length', raw.length); + request.response.add(raw); + request.response.close(); + } else { + request.response.statusCode = HttpStatus.notFound; + request.response.write('Not found'); + } + await request.response.close(); + return; + } + + final hotRestart = + request.uri.queryParameters['hotrestart'] == 'true'; + + if (hotRestart) { + stdout.write('Needs restart\n'); + request.response.write('Needs restart\n'); + } else { + request.response.write('No param found\n'); + } + await request.response.close(); + }, + ); + } + } + + Future close() async { + await _server.close(); + } + + void handleOnActiveFileChanged( + DaemonMethod method, + PreviewRequest request, + ) { + final path = method.params['path']; + try { + if (File(path).existsSync()) { + generatePreview(path); + //request.response.write('Hello, world'); + } else { + request.sendResponse( + '[{"event": "preview.error", "result": {"message: "File not found for path $path"}}]', + ); + } + } catch (e) { + request.sendResponse( + '[{"event": "preview.error", "result": {"message: "Error while generating preview $e"}}]', + ); + } + } + // [{"method": "preview.changeActiveFile", "params": { "path": "lib/preview.dart"}}] +} + +class PreviewRequest { + final String stdin; + final HttpRequest request; + + PreviewRequest({ + this.stdin, + this.request, + }); + + when({ + Function(String stdin) stdin, + Function(HttpRequest request) request, + }) { + if (this.stdin != null) stdin?.call(this.stdin); + if (this.request != null) request?.call(this.request); + } + + String getRequest() { + return ''; + } + + void sendResponse(String response) {} + + List parse() { + final request = getRequest(); + try { + final json = jsonDecode(request); + print(json); + + return List.from(json).map( + (e) { + try { + return DaemonMethod.fromJson( + Map.from(e), + ); + } catch (e) { + throw 'Invalid parsing'; + } + }, + ).toList(); + } catch (e) { + stdout + .write('[{"event": "preview.error", "result": {"message: "$e"}}]\n'); + } + + return []; + } +} + +class StdinRequest extends PreviewRequest { + final String _request; + + StdinRequest(this._request); + + String getRequest() => _request; + + void sendResponse(String response) { + stdout.write(response); + } +} + +class DaemonMethod { + final String id; + final String name; + final Map params; + + DaemonMethod({this.id, this.name, this.params}); + + factory DaemonMethod.fromJson(Map json) { + final id = json['id']; + final method = json['method']; + final params = + json['params'] != null ? Map.from(json['params']) : {}; + return DaemonMethod(id: id, name: method, params: params); + } +} +/* + +class DaemonResponse { + final String id; + final List response; + final Map args; +} + + +class DaemonEvent { + final String id; + final String name; + final Map args; +} */ diff --git a/flutter-package/bin/old/run.dart b/flutter-package/bin/old/run.dart new file mode 100644 index 0000000..cfcf6eb --- /dev/null +++ b/flutter-package/bin/old/run.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'dart:io'; + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; + +import 'package:analyzer/dart/ast/visitor.dart'; + + +import 'preview_daemon.dart'; +import '../templates.dart'; + +final featureSet = FeatureSet.fromEnableFlags([ + 'extension-methods', + //'non-nullable', +]); + +Future main() async { + final daemon = PreviewDaemonService(); + await daemon.start(); + ProcessSignal.sigint.watch().listen((_) async { + print('onClose'); + await daemon.close(); + exit(0); + }); + await daemon.listen(); +} + +class StreamResponse { + final String stdin; + final HttpRequest request; + + StreamResponse({ + this.stdin, + this.request, + }); + + when({Function(String stdin) stdin, Function(HttpRequest request) request}) { + if (this.stdin != null) stdin?.call(this.stdin); + if (this.request != null) request?.call(this.request); + } +} + +void generatePreview(String filePath) { + if (filePath == 'lib/main.preview.dart') { + File('lib/main.preview.dart').writeAsStringSync(notSeeTemplate); + stdout.write('Needs reload\n'); + return; + } + if (!filePath.startsWith('lib/')) { + File('lib/main.preview.dart').writeAsStringSync(notInLibFormatTemplate); + stdout.write('Needs reload\n'); + return; + } + if (!filePath.endsWith('.dart')) { + File('lib/main.preview.dart').writeAsStringSync(notValidFormatTemplate); + stdout.write('Needs reload\n'); + return; + } + final fileContent = File(filePath).readAsStringSync(); + + final parseResult = parseString( + content: fileContent, + featureSet: featureSet, + path: filePath, + throwIfDiagnostics: false, + ); + final providers = []; + final unit = parseResult.unit; + + final v = ExtractPreviewsVisitor(providers); + unit.visitChildren(v); + File('lib/main.preview.dart') + .writeAsStringSync(generateFile(filePath, providers)); + stdout.write('Needs reload\n'); +} + +class ExtractPreviewsVisitor extends RecursiveAstVisitor { + final List providers; + + ExtractPreviewsVisitor(this.providers); + + @override + visitClassDeclaration(ClassDeclaration node) { + final supportedExtended = 'PreviewProvider'; + final supportedMixin = 'Previewer'; + + bool isExtended() { + final extendedClass = node.extendsClause?.superclass?.toString(); + return supportedExtended == extendedClass; + } + + bool isInMixin() { + final item = node.withClause?.mixinTypes?.firstWhere( + (c) { + return supportedMixin == c.toString(); + }, + orElse: () => null, + ); + return item != null; + } + + if (isExtended() || isInMixin()) { + providers.add(node.name.toString()); + } + + return super.visitClassDeclaration(node); + } +} diff --git a/flutter-package/bin/serve.dart b/flutter-package/bin/old/serve.dart similarity index 100% rename from flutter-package/bin/serve.dart rename to flutter-package/bin/old/serve.dart diff --git a/flutter-package/lib/src/vm/vm_hot_reload.dart b/flutter-package/bin/old/vm_hot_reload.dart similarity index 100% rename from flutter-package/lib/src/vm/vm_hot_reload.dart rename to flutter-package/bin/old/vm_hot_reload.dart diff --git a/flutter-package/bin/preview.dart b/flutter-package/bin/preview.dart new file mode 100644 index 0000000..4258f7c --- /dev/null +++ b/flutter-package/bin/preview.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:shelf_static/shelf_static.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import 'daemon_service.dart'; + +import 'old/run.dart'; +import 'templates.dart'; +import 'utils.dart'; + +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_router/shelf_router.dart' as route; +import 'package:shelf_web_socket/shelf_web_socket.dart' as ws; + +import 'package:http_server/http_server.dart'; +import 'package:mime/mime.dart'; + +class Server { + HttpServer _server; + + Server(this.port); + + Future serve(Function(WebSocketChannel) websocketHandler) async { + final router = route.Router() + ..get('/ws', ws.webSocketHandler(websocketHandler)) + ..post('/screenshot', (shelf.Request request) async { + final contentType = ContentType.parse(request.headers["content-type"]); + final transformer = + MimeMultipartTransformer(contentType.parameters['boundary']); + + final bodyStream = request.read(); + final parts = await transformer.bind(bodyStream).toList(); + var params; + for (var part in parts) { + HttpMultipartFormData multipart = HttpMultipartFormData.parse(part); + + //final ContentType contentType = multipart.contentType; + final contentDisposition = multipart.contentDisposition; + final filename = contentDisposition.parameters['filename']; + + final content = multipart.cast>(); + + final filePath = "preview/screenshots/" + filename; + + if (!await Directory('preview').exists()) { + await Directory('preview').create(); + } + + if (!await Directory('preview/screenshots').exists()) { + await Directory('preview/screenshots').create(); + } + + IOSink sink = File(filePath).openWrite(); + await for (List item in content) { + sink.add(item); + } + await sink.flush(); + await sink.close(); + } + + return shelf.Response.ok(params.toString()); + }); + + final shelf.Handler staticHandler = (shelf.Request request) { + final urlPath = request.url.path; + final isGet = request.method == 'GET'; + final isAsset = urlPath.startsWith('asset/'); + + if (isGet && isAsset) { + final path = urlPath.replaceFirst('asset/', ''); + return createFileHandler(path, url: urlPath)(request); + } else { + return shelf.Response.notFound(''); + } + }; + + shelf.Handler safeNotFoundHandler(shelf.Handler handler) { + return (shelf.Request request) async { + var resp = await handler(request); + return Future.value(resp == null ? shelf.Response.notFound('') : resp); + }; + } + + final cascade = + shelf.Cascade().add(staticHandler).add(router.handler).handler; + + var handler = const shelf.Pipeline() + .addMiddleware(shelf.logRequests()) + .addHandler(safeNotFoundHandler(cascade)); + + _server = await io.serve(handler, '127.0.0.1', port); + + _server.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN'); + } + + final int port; + InternetAddress get address => _server?.address; + + Future close() { + return _server.close(); + } +} + +void main() async { + final localAddress = InternetAddress('127.0.0.1'); + final port = await getUnusedPort(localAddress); + final daemonService = DaemonService(port); + + final server = Server(port); + await server.serve((webSocket) { + daemonService.addWebSocket(webSocket); + }); + + print('Preview running on http://${server.address.host}:${server.port}'); + + await daemonService.run(); + + ProcessSignal.sigint.watch().listen((_) async { + print('onClose'); + await server.close(); + await daemonService.close(); + exit(0); + }); + + daemonService.listen(); + daemonService.sendNotification('preview.launch', {'port': port}); +} + +String activeFilePath; + +void changeActiveFile(String _activeFilePath) { + activeFilePath = _activeFilePath; + generatePreview(activeFilePath); +} + +void generatePreview(String filePath) { + if (filePath == 'lib/main.preview.dart') { + File('lib/main.preview.dart').writeAsStringSync(notSeeTemplate); + return; + } + if (!filePath.startsWith('lib/')) { + File('lib/main.preview.dart').writeAsStringSync(notInLibFormatTemplate); + return; + } + if (!filePath.endsWith('.dart')) { + File('lib/main.preview.dart').writeAsStringSync(notValidFormatTemplate); + return; + } + final fileContent = File(filePath).readAsStringSync(); + + final parseResult = parseString( + content: fileContent, + featureSet: featureSet, + path: filePath, + throwIfDiagnostics: false, + ); + final providers = []; + final unit = parseResult.unit; + + final v = ExtractPreviewsVisitor(providers); + unit.visitChildren(v); + File('lib/main.preview.dart') + .writeAsStringSync(generateFile(filePath, providers)); +} diff --git a/flutter-package/bin/run.dart b/flutter-package/bin/run.dart deleted file mode 100644 index bcb6416..0000000 --- a/flutter-package/bin/run.dart +++ /dev/null @@ -1,276 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:analyzer/dart/analysis/features.dart'; -import 'package:analyzer/dart/analysis/utilities.dart'; -import 'package:analyzer/dart/ast/ast.dart'; - -import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:async/async.dart'; - -import 'package:path/path.dart' as path; - -final featureSet = FeatureSet.fromEnableFlags([ - 'extension-methods', - //'non-nullable', -]); - -final port = 8084; - -Future main() async { - Stream cmdLine = stdin - .transform(Utf8Decoder()) - .transform(LineSplitter()) - .map((event) => StreamResponse(stdin: event)); - Stream requests; - try { - final server = await HttpServer.bind('127.0.0.1', port); - requests = server.map((event) => StreamResponse(request: event)); - - ProcessSignal.sigint.watch().listen((_) async { - print('onClose'); - await server.close(); - }); - } catch (e) { - stdout.write('error'); - stdout.write(e); - } - - final group = StreamGroup.merge([cmdLine, if (requests != null) requests]); - - await for (final response in group) { - response.when( - stdin: (file) { - try { - if (File(file).existsSync()) { - generatePreview(file); - //request.response.write('Hello, world'); - } - } catch (e) { - stdout.write('error'); - stdout.write(e); - print(e); - } - }, - request: (request) async { - if (request.uri.path.startsWith('/asset/')) { - final File file = - new File(request.uri.path.replaceFirst('/asset/', '')); - - if (file.existsSync()) { - final raw = await file.readAsBytes(); - - request.response.headers - .set('Content-Type', 'image/${path.extension(file.path)}'); - request.response.headers.set('Content-Length', raw.length); - request.response.add(raw); - request.response.close(); - } else { - request.response.statusCode = HttpStatus.notFound; - request.response.write('Not found'); - } - await request.response.close(); - return; - } - - final hotRestart = request.uri.queryParameters['hotrestart'] == 'true'; - - if (hotRestart) { - stdout.write('Needs restart\n'); - request.response.write('Needs restart\n'); - } else { - request.response.write('No param found\n'); - } - await request.response.close(); - }, - ); - } -} - -class StreamResponse { - final String stdin; - final HttpRequest request; - - StreamResponse({ - this.stdin, - this.request, - }); - - when({Function(String stdin) stdin, Function(HttpRequest request) request}) { - if (this.stdin != null) stdin?.call(this.stdin); - if (this.request != null) request?.call(this.request); - } -} - -void generatePreview(String filePath) { - if (filePath == 'lib/main.preview.dart') { - File('lib/main.preview.dart').writeAsStringSync(notSeeTemplate); - stdout.write('Needs reload\n'); - return; - } - if (!filePath.startsWith('lib/')) { - File('lib/main.preview.dart').writeAsStringSync(notInLibFormatTemplate); - stdout.write('Needs reload\n'); - return; - } - if (!filePath.endsWith('.dart')) { - File('lib/main.preview.dart').writeAsStringSync(notValidFormatTemplate); - stdout.write('Needs reload\n'); - return; - } - final fileContent = File(filePath).readAsStringSync(); - - final parseResult = parseString( - content: fileContent, - featureSet: featureSet, - path: filePath, - throwIfDiagnostics: false, - ); - final providers = []; - final unit = parseResult.unit; - - final v = ExtractPreviewsVisitor(providers); - unit.visitChildren(v); - File('lib/main.preview.dart') - .writeAsStringSync(generateFile(filePath, providers)); - stdout.write('Needs reload\n'); -} - -class ExtractPreviewsVisitor extends RecursiveAstVisitor { - final List providers; - - ExtractPreviewsVisitor(this.providers); - - @override - visitClassDeclaration(ClassDeclaration node) { - final supportedExtended = 'PreviewProvider'; - final supportedMixin = 'Previewer'; - - bool isExtended() { - final extendedClass = node.extendsClause?.superclass?.toString(); - return supportedExtended == extendedClass; - } - - bool isInMixin() { - final item = node.withClause?.mixinTypes?.firstWhere( - (c) { - return supportedMixin == c.toString(); - }, - orElse: () => null, - ); - return item != null; - } - - if (isExtended() || isInMixin()) { - providers.add(node.name.toString()); - } - - return super.visitClassDeclaration(node); - } -} - -String generateFile(String filePath, List providers) { - final import = filePath.replaceFirst('/lib/', '').replaceFirst('lib/', ''); - return ''' - -import 'package:flutter/widgets.dart'; -import 'package:preview/preview.dart'; -import '$import'; -void main() { - runApp(_PreviewApp()); -} - -class _PreviewApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return PreviewPage( - path: '$import', - providers: () => [ - ${providers.reversed.fold('', (p, e) => e + '(), \n ' + p)} - ], - ); - } -} - '''; -} - -const String notSeeTemplate = ''' - -import 'package:flutter/widgets.dart'; -import 'package:preview/preview.dart'; - -// -// -// ............. -// ************ -// .************ -// ************ -// .************ -// ************ -// .************ -// ************ -// ************ ************ -// ******* ,************ -// .** ////******** -// *////////**** -// ,//////////%% -// //////%%%%%% -// ,%%%%%%%%%%%% -// %%%%%%%%%%%% -// -// -// -// - -void main() { - runApp(_PreviewApp()); -} - -class _PreviewApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return PreviewPage( - child: Text('You should not be editing this file 👀') - ); - } -} - '''; - -const String notValidFormatTemplate = ''' - -import 'package:flutter/widgets.dart'; -import 'package:preview/preview.dart'; - -void main() { - runApp(_PreviewApp()); -} - -class _PreviewApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return PreviewPage( - child: Text('This format does not support preview 👀') - ); - } -} - '''; - -const String notInLibFormatTemplate = ''' - -import 'package:flutter/widgets.dart'; -import 'package:preview/preview.dart'; - -void main() { - runApp(_PreviewApp()); -} - -class _PreviewApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return PreviewPage( - child: Text('Select a file from /lib folder to see the preview 👀'), - ); - } -} - '''; diff --git a/flutter-package/bin/templates.dart b/flutter-package/bin/templates.dart new file mode 100644 index 0000000..05f9bcf --- /dev/null +++ b/flutter-package/bin/templates.dart @@ -0,0 +1,107 @@ +String generateFile( + String filePath, + List providers, +) { + final import = filePath.replaceFirst('/lib/', '').replaceFirst('lib/', ''); + return ''' + +import 'package:flutter/widgets.dart'; +import 'package:preview/preview.dart'; +import '$import'; +void main() { + runApp(_PreviewApp()); +} + +class _PreviewApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return PreviewPage( + path: '$import', + providers: () => [ + ${providers.reversed.fold('', (p, e) => e + '(), \n ' + p)} + ], + ); + } +} + '''; +} + +const String notSeeTemplate = ''' + +import 'package:flutter/widgets.dart'; +import 'package:preview/preview.dart'; + +// +// +// ............. +// ************ +// .************ +// ************ +// .************ +// ************ +// .************ +// ************ +// ************ ************ +// ******* ,************ +// .** ////******** +// *////////**** +// ,//////////%% +// //////%%%%%% +// ,%%%%%%%%%%%% +// %%%%%%%%%%%% +// +// +// +// + +void main() { + runApp(_PreviewApp()); +} + +class _PreviewApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return PreviewPage( + child: Text('You should not be editing this file 👀') + ); + } +} + '''; + +const String notValidFormatTemplate = ''' + +import 'package:flutter/widgets.dart'; +import 'package:preview/preview.dart'; + +void main() { + runApp(_PreviewApp()); +} + +class _PreviewApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return PreviewPage( + child: Text('This format does not support preview 👀') + ); + } +} + '''; + +const String notInLibFormatTemplate = ''' + +import 'package:flutter/widgets.dart'; +import 'package:preview/preview.dart'; + +void main() { + runApp(_PreviewApp()); +} + +class _PreviewApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return PreviewPage( + child: Text('Select a file from /lib folder to see the preview 👀'), + ); + } +} + '''; diff --git a/flutter-package/bin/utils.dart b/flutter-package/bin/utils.dart new file mode 100644 index 0000000..7f362af --- /dev/null +++ b/flutter-package/bin/utils.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +Future getUnusedPort(InternetAddress address) { + return ServerSocket.bind(address ?? InternetAddress.anyIPv4, 0) + .then((socket) { + var port = socket.port; + socket.close(); + return port; + }); +} diff --git a/flutter-package/example/android/app/src/main/AndroidManifest.xml b/flutter-package/example/android/app/src/main/AndroidManifest.xml index 99ed3c3..c627ab0 100644 --- a/flutter-package/example/android/app/src/main/AndroidManifest.xml +++ b/flutter-package/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,7 @@ + +