| // 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') | |
| ..addOption('depfile') | |
| ..addOption('exclude-pattern') | |
| ..addOption('build-dir'); | |
| 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_impl' if (dart.library.html) 'dart:_skwasm_stub';", | |
| '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; | |
| final String? excludePattern = results['exclude-pattern'] as String?; | |
| final String stampfilePath = results['stamp'] as String; | |
| final String depfilePath = results['depfile'] as String; | |
| final String buildDirPath = results['build-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); | |
| } | |
| final List<String> inputFiles = <String>[]; | |
| final List<FileSystemEntity> entries = Directory(inputDirectoryPath).listSync( | |
| recursive: true, followLinks: false, | |
| ); | |
| for (final File inputFile in entries.whereType<File>()) { | |
| if (excludePattern != null && inputFile.path.startsWith(excludePattern)) { | |
| continue; | |
| } | |
| if (!inputFile.path.endsWith('.dart') || inputFile.path.endsWith('_test.dart')) { | |
| continue; | |
| } | |
| inputFiles.add(path.relative(inputFile.path, from: buildDirPath)); | |
| String pathSuffix = inputFile.path.substring(inputDirectoryPath.length); | |
| if (libraryName != null) { | |
| pathSuffix = path.join(libraryName, pathSuffix); | |
| } | |
| final String outputFilePath = path.join(directory.path, pathSuffix); | |
| processFile(inputFile.path, 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 | |
| ); | |
| } | |
| File(stampfilePath).writeAsStringSync('stamp'); | |
| writeDepfile(depfilePath, stampfilePath, inputFiles); | |
| } | |
| void writeDepfile(String depfilePath, String stampfilePath, List<String> inputFiles) { | |
| final StringBuffer outBuf = StringBuffer(); | |
| outBuf.write('$stampfilePath: '); | |
| outBuf.write(inputFiles.join(' ')); | |
| File(depfilePath).writeAsStringSync(outBuf.toString()); | |
| } | |
| 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); | |
| } | |
| } |