| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:path/path.dart' as path; |
| |
| final ArgParser argParser = ArgParser() |
| ..addOption('output-dir') |
| ..addOption('input-dir') |
| ..addFlag('ui') |
| ..addFlag('public') |
| ..addOption('library-name') |
| ..addOption('api-file') |
| ..addMultiOption('source-file') |
| ..addOption('stamp'); |
| |
| final List<Replacer> uiPatterns = <Replacer>[ |
| AllReplacer(RegExp(r'library\s+ui;'), 'library dart.ui;'), |
| AllReplacer(RegExp(r'part\s+of\s+ui;'), 'part of dart.ui;'), |
| |
| // import 'src/engine.dart' as engine; |
| AllReplacer(RegExp(r''' |
| import\s*'src/engine.dart'\s*as\s+engine; |
| '''), r''' |
| import 'dart:_engine' as engine; |
| '''), |
| |
| // import 'ui_web/src/ui_web.dart' as ui_web; |
| AllReplacer(RegExp(r''' |
| import\s*'ui_web/src/ui_web.dart'\s*as\s+ui_web; |
| '''), r''' |
| import 'dart:ui_web' as ui_web; |
| '''), |
| ]; |
| |
| List<Replacer> generateApiFilePatterns(String libraryName, bool isPublic, List<String> extraImports) { |
| final String libraryPrefix = isPublic ? '' : '_'; |
| return <Replacer>[ |
| AllReplacer(RegExp('library\\s+$libraryName;'), ''' |
| @JS() |
| library dart.$libraryPrefix$libraryName; |
| |
| import 'dart:async'; |
| import 'dart:collection'; |
| import 'dart:convert' hide Codec; |
| import 'dart:developer' as developer; |
| import 'dart:js_util' as js_util; |
| import 'dart:_js_annotations'; |
| import 'dart:js_interop' hide JS; |
| import 'dart:js_interop_unsafe'; |
| import 'dart:math' as math; |
| import 'dart:typed_data'; |
| import 'dart:ui' as ui; |
| ${extraImports.join('\n')} |
| ''' |
| ), |
| // Replace exports of engine files with "part" directives. |
| MappedReplacer(RegExp(''' |
| export\\s*'$libraryName/(.*)'; |
| '''), (Match match) => ''' |
| part '$libraryName/${match.group(1)}'; |
| ''' |
| ), |
| ]; |
| } |
| |
| List<Replacer> generatePartsPatterns(String libraryName, bool isPublic) { |
| final String libraryPrefix = isPublic ? '' : '_'; |
| return <Replacer>[ |
| AllReplacer(RegExp('part\\s+of\\s+$libraryName;'), 'part of dart.$libraryPrefix$libraryName;'), |
| // Remove library-level JS annotations. |
| AllReplacer(RegExp(r'\n@JS(.*)\nlibrary .+;'), ''), |
| // Remove library directives. |
| AllReplacer(RegExp(r'\nlibrary .+;'), ''), |
| // Remove imports/exports from all part files. |
| AllReplacer(RegExp(r'\nimport\s*.*'), ''), |
| AllReplacer(RegExp(r'\nexport\s*.*'), ''), |
| AllReplacer(RegExp(r'\n@DefaultAsset(.*)'), ''), |
| ]; |
| } |
| |
| final List<Replacer> stripMetaPatterns = <Replacer>[ |
| AllReplacer(RegExp(r"import\s*'package:meta/meta.dart';"), ''), |
| AllReplacer('@required', ''), |
| AllReplacer('@protected', ''), |
| AllReplacer('@mustCallSuper', ''), |
| AllReplacer('@immutable', ''), |
| AllReplacer('@visibleForTesting', ''), |
| ]; |
| |
| const Set<String> rootLibraryNames = <String>{ |
| 'ui_web', |
| 'engine', |
| 'skwasm_stub', |
| 'skwasm_impl', |
| }; |
| |
| final Map<Pattern, String> extraImportsMap = <Pattern, String>{ |
| RegExp('skwasm_(stub|impl)'): "import 'dart:_skwasm_stub' if (dart.library.ffi) 'dart:_skwasm_impl';", |
| 'ui_web': "import 'dart:ui_web' as ui_web;", |
| 'engine': "import 'dart:_engine';", |
| 'web_unicode': "import 'dart:_web_unicode';", |
| 'web_test_fonts': "import 'dart:_web_test_fonts';", |
| 'web_locale_keymap': "import 'dart:_web_locale_keymap' as locale_keymap;", |
| }; |
| |
| // Rewrites the "package"-style web ui library into a dart:ui implementation. |
| // So far this only requires a replace of the library declarations. |
| void main(List<String> arguments) { |
| final ArgResults results = argParser.parse(arguments); |
| final Directory directory = Directory(results['output-dir'] as String); |
| final String inputDirectoryPath = results['input-dir'] as String; |
| |
| String Function(String source)? preprocessor; |
| List<Replacer> replacementPatterns; |
| String? libraryName; |
| |
| final bool isPublic = results['public'] as bool; |
| |
| if (results['ui'] as bool) { |
| replacementPatterns = uiPatterns; |
| } else { |
| libraryName = results['library-name'] as String?; |
| if (libraryName == null) { |
| throw Exception('library-name must be specified if not rewriting ui'); |
| } |
| preprocessor = (String source) => preprocessPartFile(source, libraryName!); |
| replacementPatterns = generatePartsPatterns(libraryName, isPublic); |
| } |
| for (final String inputFilePath in results['source-file'] as Iterable<String>) { |
| String pathSuffix = inputFilePath.substring(inputDirectoryPath.length); |
| if (libraryName != null) { |
| pathSuffix = path.join(libraryName, pathSuffix); |
| } |
| final String outputFilePath = path.join(directory.path, pathSuffix); |
| processFile(inputFilePath, outputFilePath, preprocessor, replacementPatterns); |
| } |
| |
| if (results['api-file'] != null) { |
| if (libraryName == null) { |
| throw Exception('library-name must be specified if api-file is specified'); |
| } |
| |
| final String inputFilePath = results['api-file'] as String; |
| final String outputFilePath = path.join( |
| directory.path, path.basename(inputFilePath)); |
| |
| final List<String> extraImports = getExtraImportsForLibrary(libraryName); |
| replacementPatterns = generateApiFilePatterns(libraryName, isPublic, extraImports); |
| |
| processFile( |
| inputFilePath, |
| outputFilePath, |
| (String source) => validateApiFile(inputFilePath, source, libraryName!), |
| replacementPatterns |
| ); |
| } |
| |
| |
| if (results['stamp'] != null) { |
| File(results['stamp'] as String).writeAsStringSync('stamp'); |
| } |
| } |
| |
| List<String> getExtraImportsForLibrary(String libraryName) { |
| // Only our root libraries should have extra imports. |
| if (!rootLibraryNames.contains(libraryName)) { |
| return <String>[]; |
| } |
| |
| final List<String> extraImports = <String>[]; |
| for (final MapEntry<Pattern, String> entry in extraImportsMap.entries) { |
| // A library shouldn't import itself. |
| if (entry.key.matchAsPrefix(libraryName) == null) { |
| extraImports.add(entry.value); |
| } |
| } |
| if (libraryName == 'skwasm_impl') { |
| extraImports.add("import 'dart:_wasm';"); |
| } |
| return extraImports; |
| } |
| |
| void processFile(String inputFilePath, String outputFilePath, String Function(String source)? preprocessor, List<Replacer> replacementPatterns) { |
| final File inputFile = File(inputFilePath); |
| final File outputFile = File(outputFilePath) |
| ..createSync(recursive: true); |
| outputFile.writeAsStringSync(processSource( |
| inputFile.readAsStringSync(), |
| preprocessor, |
| replacementPatterns)); |
| } |
| |
| String processSource(String source, String Function(String source)? preprocessor, List<Replacer> replacementPatterns) { |
| if (preprocessor != null) { |
| source = preprocessor(source); |
| } |
| for (final Replacer replacer in stripMetaPatterns) { |
| source = replacer.perform(source); |
| } |
| for (final Replacer replacer in replacementPatterns) { |
| source = replacer.perform(source); |
| } |
| return source; |
| } |
| |
| // Enforces a particular structure in top level api files for sublibraries. |
| // |
| // Code in api files must only be made of the library directive, exports, |
| // and code comments. Imports are disallowed. Instead, the required imports are |
| // added by this script during the rewrite. |
| String validateApiFile(String apiFilePath, String apiFileCode, String libraryName) { |
| final List<String> expectedLines = <String>[ |
| 'library $libraryName;', |
| ]; |
| |
| final List<String> lines = apiFileCode.split('\n'); |
| for (int i = 0; i < lines.length; i += 1) { |
| final int lineNumber = i + 1; |
| final String line = lines[i].trim(); |
| |
| if (line.isEmpty) { |
| // Emply lines are OK |
| continue; |
| } |
| |
| if (expectedLines.contains(line)) { |
| // Expected; let it pass. |
| continue; |
| } |
| |
| if (line.startsWith('//')) { |
| // Comments are OK |
| continue; |
| } |
| |
| if (line.startsWith('export')) { |
| // Exports are OK |
| continue; |
| } |
| |
| if (line.startsWith('@DefaultAsset')) { |
| // Default asset annotations are OK |
| continue; |
| } |
| |
| if (line.startsWith("import 'dart:ffi';")) { |
| // dart:ffi import is an exception to the import rule, since the |
| // @DefaultAsset annotation comes from dart:ffi. |
| continue; |
| } |
| |
| throw Exception( |
| 'on line $lineNumber: unexpected code in $apiFilePath. This file ' |
| 'may only contain comments and exports. Found:\n' |
| '$line' |
| ); |
| } |
| return apiFileCode; |
| } |
| |
| String preprocessPartFile(String source, String libraryName) { |
| return 'part of $libraryName;\n$source'; |
| } |
| |
| /// Responsible for performing string replacements. |
| abstract class Replacer { |
| /// Performs the replacement in the provided [text]. |
| String perform(String text); |
| } |
| |
| /// Replaces all occurrences of a pattern with a fixed string. |
| class AllReplacer implements Replacer { |
| /// Creates a new tuple with the given [pattern] and [replacement] string. |
| AllReplacer(this._pattern, this._replacement); |
| |
| /// The pattern to be replaced. |
| final Pattern _pattern; |
| |
| /// The replacement string. |
| final String _replacement; |
| |
| @override |
| String perform(String text) { |
| return text.replaceAll(_pattern, _replacement); |
| } |
| } |
| |
| /// Uses a callback to replace each occurrence of a pattern. |
| class MappedReplacer implements Replacer { |
| MappedReplacer(this._pattern, this._replace); |
| |
| /// The pattern to be replaced. |
| final RegExp _pattern; |
| |
| /// A callback to replace each occurrence of [_pattern]. |
| final String Function(Match match) _replace; |
| |
| @override |
| String perform(String text) { |
| return text.replaceAllMapped(_pattern, _replace); |
| } |
| } |