
When converting all of the samples to use the snippet tool, I encountered some bugs/shortcomings: 1. The document production took 90 minutes, since the snippet tool was being invoked from the command line each time. I fixed this by snapshotting the executable before running, so it's down to 7 minutes. 2. The sample code was not being properly escaped by the snippet tool, so generics were causing issues in the HTML output. It is now quoted. 3. Code examples that used languages other than Dart were not supported. Anything that highlight.js was compiled for dartdoc with is now supported. 4. The comment color for highlight.js was light grey on white, which was pretty unreadable. It's now dark green and bold.
459 lines
16 KiB
Dart
459 lines
16 KiB
Dart
// Copyright 2016 The Chromium 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:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:args/args.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
const String kDocsRoot = 'dev/docs';
|
|
const String kPublishRoot = '$kDocsRoot/doc';
|
|
|
|
/// This script expects to run with the cwd as the root of the flutter repo. It
|
|
/// will generate documentation for the packages in `//packages/` and write the
|
|
/// documentation to `//dev/docs/doc/api/`.
|
|
///
|
|
/// This script also updates the index.html file so that it can be placed
|
|
/// at the root of docs.flutter.io. We are keeping the files inside of
|
|
/// docs.flutter.io/flutter for now, so we need to manipulate paths
|
|
/// a bit. See https://github.com/flutter/flutter/issues/3900 for more info.
|
|
///
|
|
/// This will only work on UNIX systems, not Windows. It requires that 'git' be
|
|
/// in your path. It requires that 'flutter' has been run previously. It uses
|
|
/// the version of Dart downloaded by the 'flutter' tool in this repository and
|
|
/// will crash if that is absent.
|
|
Future<void> main(List<String> arguments) async {
|
|
final ArgParser argParser = _createArgsParser();
|
|
final ArgResults args = argParser.parse(arguments);
|
|
if (args['help']) {
|
|
print ('Usage:');
|
|
print (argParser.usage);
|
|
exit(0);
|
|
}
|
|
// If we're run from the `tools` dir, set the cwd to the repo root.
|
|
if (path.basename(Directory.current.path) == 'tools')
|
|
Directory.current = Directory.current.parent.parent;
|
|
|
|
final ProcessResult flutter = Process.runSync('flutter', <String>[]);
|
|
final File versionFile = File('version');
|
|
if (flutter.exitCode != 0 || !versionFile.existsSync())
|
|
throw Exception('Failed to determine Flutter version.');
|
|
final String version = versionFile.readAsStringSync();
|
|
|
|
// Create the pubspec.yaml file.
|
|
final StringBuffer buf = StringBuffer();
|
|
buf.writeln('name: Flutter');
|
|
buf.writeln('homepage: https://flutter.io');
|
|
buf.writeln('version: $version');
|
|
buf.writeln('dependencies:');
|
|
for (String package in findPackageNames()) {
|
|
buf.writeln(' $package:');
|
|
buf.writeln(' sdk: flutter');
|
|
}
|
|
buf.writeln(' platform_integration: 0.0.1');
|
|
buf.writeln('dependency_overrides:');
|
|
buf.writeln(' platform_integration:');
|
|
buf.writeln(' path: platform_integration');
|
|
File('$kDocsRoot/pubspec.yaml').writeAsStringSync(buf.toString());
|
|
|
|
// Create the library file.
|
|
final Directory libDir = Directory('$kDocsRoot/lib');
|
|
libDir.createSync();
|
|
|
|
final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
|
|
for (String libraryRef in libraryRefs()) {
|
|
contents.writeln('import \'package:$libraryRef\';');
|
|
}
|
|
File('$kDocsRoot/lib/temp_doc.dart').writeAsStringSync(contents.toString());
|
|
|
|
final String flutterRoot = Directory.current.path;
|
|
final Map<String, String> pubEnvironment = <String, String>{
|
|
'FLUTTER_ROOT': flutterRoot,
|
|
};
|
|
|
|
// If there's a .pub-cache dir in the flutter root, use that.
|
|
final String pubCachePath = '$flutterRoot/.pub-cache';
|
|
if (Directory(pubCachePath).existsSync()) {
|
|
pubEnvironment['PUB_CACHE'] = pubCachePath;
|
|
}
|
|
|
|
final String pubExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/pub';
|
|
|
|
// Run pub.
|
|
Process process = await Process.start(
|
|
pubExecutable,
|
|
<String>['get'],
|
|
workingDirectory: kDocsRoot,
|
|
environment: pubEnvironment,
|
|
);
|
|
printStream(process.stdout, prefix: 'pub:stdout: ');
|
|
printStream(process.stderr, prefix: 'pub:stderr: ');
|
|
final int code = await process.exitCode;
|
|
if (code != 0)
|
|
exit(code);
|
|
|
|
createFooter('$kDocsRoot/lib/footer.html');
|
|
copyAssets();
|
|
cleanOutSnippets();
|
|
precompileSnippetsTool();
|
|
|
|
final List<String> dartdocBaseArgs = <String>['global', 'run'];
|
|
if (args['checked']) {
|
|
dartdocBaseArgs.add('-c');
|
|
}
|
|
dartdocBaseArgs.add('dartdoc');
|
|
|
|
// Verify which version of dartdoc we're using.
|
|
final ProcessResult result = Process.runSync(
|
|
pubExecutable,
|
|
<String>[]..addAll(dartdocBaseArgs)..add('--version'),
|
|
workingDirectory: kDocsRoot,
|
|
environment: pubEnvironment,
|
|
);
|
|
print('\n${result.stdout}flutter version: $version\n');
|
|
|
|
if (args['json']) {
|
|
dartdocBaseArgs.add('--json');
|
|
}
|
|
if (args['validate-links']) {
|
|
dartdocBaseArgs.add('--validate-links');
|
|
} else {
|
|
dartdocBaseArgs.add('--no-validate-links');
|
|
}
|
|
// Generate the documentation.
|
|
// We don't need to exclude flutter_tools in this list because it's not in the
|
|
// recursive dependencies of the package defined at dev/docs/pubspec.yaml
|
|
final List<String> dartdocArgs = <String>[]..addAll(dartdocBaseArgs)..addAll(<String>[
|
|
'--inject-html',
|
|
'--header', 'styles.html',
|
|
'--header', 'analytics.html',
|
|
'--header', 'survey.html',
|
|
'--header', 'snippets.html',
|
|
'--footer-text', 'lib/footer.html',
|
|
'--exclude-packages',
|
|
<String>[
|
|
'analyzer',
|
|
'args',
|
|
'barback',
|
|
'cli_util',
|
|
'csslib',
|
|
'flutter_goldens',
|
|
'front_end',
|
|
'fuchsia_remote_debug_protocol',
|
|
'glob',
|
|
'html',
|
|
'http_multi_server',
|
|
'io',
|
|
'isolate',
|
|
'js',
|
|
'kernel',
|
|
'logging',
|
|
'mime',
|
|
'mockito',
|
|
'node_preamble',
|
|
'plugin',
|
|
'shelf',
|
|
'shelf_packages_handler',
|
|
'shelf_static',
|
|
'shelf_web_socket',
|
|
'utf',
|
|
'watcher',
|
|
'yaml',
|
|
].join(','),
|
|
'--exclude',
|
|
<String>[
|
|
'package:Flutter/temp_doc.dart',
|
|
'package:http/browser_client.dart',
|
|
'package:intl/intl_browser.dart',
|
|
'package:matcher/mirror_matchers.dart',
|
|
'package:quiver/io.dart',
|
|
'package:quiver/mirrors.dart',
|
|
'package:vm_service_client/vm_service_client.dart',
|
|
'package:web_socket_channel/html.dart',
|
|
].join(','),
|
|
'--favicon=favicon.ico',
|
|
'--package-order', 'flutter,Dart,flutter_test,flutter_driver',
|
|
'--auto-include-dependencies',
|
|
]);
|
|
|
|
String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
|
|
print('Executing: (cd $kDocsRoot ; $pubExecutable ${dartdocArgs.map<String>(quote).join(' ')})');
|
|
|
|
process = await Process.start(
|
|
pubExecutable,
|
|
dartdocArgs,
|
|
workingDirectory: kDocsRoot,
|
|
environment: pubEnvironment,
|
|
);
|
|
printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
|
|
filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
|
|
RegExp(r'^generating docs for library '), // unnecessary verbosity
|
|
RegExp(r'^pars'), // unnecessary verbosity
|
|
],
|
|
);
|
|
printStream(process.stderr, prefix: args['json'] ? '' : 'dartdoc:stderr: ',
|
|
filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
|
|
RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/pub.dartlang.org/.+\)'), // packages outside our control
|
|
],
|
|
);
|
|
final int exitCode = await process.exitCode;
|
|
|
|
if (exitCode != 0)
|
|
exit(exitCode);
|
|
|
|
sanityCheckDocs();
|
|
|
|
createIndexAndCleanup();
|
|
}
|
|
|
|
ArgParser _createArgsParser() {
|
|
final ArgParser parser = ArgParser();
|
|
parser.addFlag('help', abbr: 'h', negatable: false,
|
|
help: 'Show command help.');
|
|
parser.addFlag('verbose', negatable: true, defaultsTo: true,
|
|
help: 'Whether to report all error messages (on) or attempt to '
|
|
'filter out some known false positives (off). Shut this off '
|
|
'locally if you want to address Flutter-specific issues.');
|
|
parser.addFlag('checked', abbr: 'c', negatable: true,
|
|
help: 'Run dartdoc in checked mode.');
|
|
parser.addFlag('json', negatable: true,
|
|
help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.');
|
|
parser.addFlag('validate-links', negatable: true,
|
|
help: 'Display warnings for broken links generated by dartdoc (slow)');
|
|
return parser;
|
|
}
|
|
|
|
final RegExp gitBranchRegexp = RegExp(r'^## (.*)');
|
|
|
|
void createFooter(String footerPath) {
|
|
const int kGitRevisionLength = 10;
|
|
|
|
ProcessResult gitResult = Process.runSync('git', <String>['rev-parse', 'HEAD']);
|
|
if (gitResult.exitCode != 0)
|
|
throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}';
|
|
String gitRevision = gitResult.stdout.trim();
|
|
|
|
gitResult = Process.runSync('git', <String>['status', '-b', '--porcelain']);
|
|
if (gitResult.exitCode != 0)
|
|
throw 'git status exit with non-zero exit code: ${gitResult.exitCode}';
|
|
final Match gitBranchMatch = gitBranchRegexp.firstMatch(
|
|
gitResult.stdout.trim().split('\n').first);
|
|
final String gitBranchOut = gitBranchMatch == null ? '' : '• </span class="no-break">${gitBranchMatch.group(1).split('...').first}</span>';
|
|
|
|
gitRevision = gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
|
|
|
|
final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
|
|
|
|
File(footerPath).writeAsStringSync(<String>[
|
|
'• </span class="no-break">$timestamp<span>',
|
|
'• </span class="no-break">$gitRevision</span>',
|
|
gitBranchOut].join(' '));
|
|
}
|
|
|
|
/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied], if
|
|
/// specified, for each source/destination file pair.
|
|
///
|
|
/// Creates `destDir` if needed.
|
|
void copyDirectorySync(Directory srcDir, Directory destDir, [void onFileCopied(File srcFile, File destFile)]) {
|
|
if (!srcDir.existsSync())
|
|
throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
|
|
|
|
if (!destDir.existsSync())
|
|
destDir.createSync(recursive: true);
|
|
|
|
for (FileSystemEntity entity in srcDir.listSync()) {
|
|
final String newPath = path.join(destDir.path, path.basename(entity.path));
|
|
if (entity is File) {
|
|
final File newFile = File(newPath);
|
|
entity.copySync(newPath);
|
|
onFileCopied?.call(entity, newFile);
|
|
} else if (entity is Directory) {
|
|
copyDirectorySync(entity, Directory(newPath));
|
|
} else {
|
|
throw Exception('${entity.path} is neither File nor Directory');
|
|
}
|
|
}
|
|
}
|
|
|
|
void copyAssets() {
|
|
final Directory assetsDir = Directory(path.join(kPublishRoot, 'assets'));
|
|
if (assetsDir.existsSync()) {
|
|
assetsDir.deleteSync(recursive: true);
|
|
}
|
|
copyDirectorySync(
|
|
Directory(path.join(kDocsRoot, 'assets')),
|
|
Directory(path.join(kPublishRoot, 'assets')),
|
|
(File src, File dest) => print('Copied ${src.path} to ${dest.path}'));
|
|
}
|
|
|
|
|
|
void cleanOutSnippets() {
|
|
final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
|
|
if (snippetsDir.existsSync()) {
|
|
snippetsDir
|
|
..deleteSync(recursive: true)
|
|
..createSync(recursive: true);
|
|
}
|
|
}
|
|
|
|
File precompileSnippetsTool() {
|
|
final File snapshotPath = File(path.join('bin', 'cache', 'snippets.snapshot'));
|
|
print('Precompiling snippets tool into ${snapshotPath.absolute.path}');
|
|
if (snapshotPath.existsSync()) {
|
|
snapshotPath.deleteSync();
|
|
}
|
|
// In order to be able to optimize properly, we need to provide a training set
|
|
// of arguments, and an input file to process.
|
|
final Directory tempDir = Directory.systemTemp.createTempSync('dartdoc_snippet_');
|
|
final File trainingFile = File(path.join(tempDir.path, 'snippet_training'));
|
|
trainingFile.writeAsStringSync('```dart\nvoid foo(){}\n```');
|
|
Process.runSync(Platform.resolvedExecutable, <String>[
|
|
'--snapshot=${snapshotPath.absolute.path}',
|
|
'--snapshot_kind=app-jit',
|
|
path.join(
|
|
'dev',
|
|
'snippets',
|
|
'lib',
|
|
'main.dart',
|
|
),
|
|
'--type=sample',
|
|
'--input=${trainingFile.absolute.path}',
|
|
'--output=${path.join(tempDir.absolute.path, 'training_output.txt')}',
|
|
]);
|
|
tempDir.deleteSync(recursive: true);
|
|
return snapshotPath;
|
|
}
|
|
|
|
void sanityCheckDocs() {
|
|
final List<String> canaries = <String>[
|
|
'$kPublishRoot/assets/overrides.css',
|
|
'$kPublishRoot/api/dart-io/File-class.html',
|
|
'$kPublishRoot/api/dart-ui/Canvas-class.html',
|
|
'$kPublishRoot/api/dart-ui/Canvas/drawRect.html',
|
|
'$kPublishRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
|
|
'$kPublishRoot/api/flutter_test/WidgetTester/pumpWidget.html',
|
|
'$kPublishRoot/api/material/Material-class.html',
|
|
'$kPublishRoot/api/material/Tooltip-class.html',
|
|
'$kPublishRoot/api/widgets/Widget-class.html',
|
|
];
|
|
for (String canary in canaries) {
|
|
if (!File(canary).existsSync())
|
|
throw Exception('Missing "$canary", which probably means the documentation failed to build correctly.');
|
|
}
|
|
}
|
|
|
|
/// Creates a custom index.html because we try to maintain old
|
|
/// paths. Cleanup unused index.html files no longer needed.
|
|
void createIndexAndCleanup() {
|
|
print('\nCreating a custom index.html in $kPublishRoot/index.html');
|
|
removeOldFlutterDocsDir();
|
|
renameApiDir();
|
|
copyIndexToRootOfDocs();
|
|
addHtmlBaseToIndex();
|
|
changePackageToSdkInTitlebar();
|
|
putRedirectInOldIndexLocation();
|
|
print('\nDocs ready to go!');
|
|
}
|
|
|
|
void removeOldFlutterDocsDir() {
|
|
try {
|
|
Directory('$kPublishRoot/flutter').deleteSync(recursive: true);
|
|
} on FileSystemException {
|
|
// If the directory does not exist, that's OK.
|
|
}
|
|
}
|
|
|
|
void renameApiDir() {
|
|
Directory('$kPublishRoot/api').renameSync('$kPublishRoot/flutter');
|
|
}
|
|
|
|
void copyIndexToRootOfDocs() {
|
|
File('$kPublishRoot/flutter/index.html').copySync('$kPublishRoot/index.html');
|
|
}
|
|
|
|
void changePackageToSdkInTitlebar() {
|
|
final File indexFile = File('$kPublishRoot/index.html');
|
|
String indexContents = indexFile.readAsStringSync();
|
|
indexContents = indexContents.replaceFirst(
|
|
'<li><a href="https://flutter.io">Flutter package</a></li>',
|
|
'<li><a href="https://flutter.io">Flutter SDK</a></li>',
|
|
);
|
|
|
|
indexFile.writeAsStringSync(indexContents);
|
|
}
|
|
|
|
void addHtmlBaseToIndex() {
|
|
final File indexFile = File('$kPublishRoot/index.html');
|
|
String indexContents = indexFile.readAsStringSync();
|
|
indexContents = indexContents.replaceFirst(
|
|
'</title>\n',
|
|
'</title>\n <base href="./flutter/">\n',
|
|
);
|
|
indexContents = indexContents.replaceAll(
|
|
'href="Android/Android-library.html"',
|
|
'href="/javadoc/"',
|
|
);
|
|
indexContents = indexContents.replaceAll(
|
|
'href="iOS/iOS-library.html"',
|
|
'href="/objcdoc/"',
|
|
);
|
|
|
|
indexFile.writeAsStringSync(indexContents);
|
|
}
|
|
|
|
void putRedirectInOldIndexLocation() {
|
|
const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
|
|
File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag);
|
|
}
|
|
|
|
List<String> findPackageNames() {
|
|
return findPackages().map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
|
|
}
|
|
|
|
/// Finds all packages in the Flutter SDK
|
|
List<FileSystemEntity> findPackages() {
|
|
return Directory('packages')
|
|
.listSync()
|
|
.where((FileSystemEntity entity) {
|
|
if (entity is! Directory)
|
|
return false;
|
|
final File pubspec = File('${entity.path}/pubspec.yaml');
|
|
// TODO(ianh): Use a real YAML parser here
|
|
return !pubspec.readAsStringSync().contains('nodoc: true');
|
|
})
|
|
.cast<Directory>()
|
|
.toList();
|
|
}
|
|
|
|
/// Returns import or on-disk paths for all libraries in the Flutter SDK.
|
|
Iterable<String> libraryRefs() sync* {
|
|
for (Directory dir in findPackages()) {
|
|
final String dirName = path.basename(dir.path);
|
|
for (FileSystemEntity file in Directory('${dir.path}/lib').listSync()) {
|
|
if (file is File && file.path.endsWith('.dart')) {
|
|
yield '$dirName/${path.basename(file.path)}';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add a fake package for platform integration APIs.
|
|
yield 'platform_integration/android.dart';
|
|
yield 'platform_integration/ios.dart';
|
|
}
|
|
|
|
void printStream(Stream<List<int>> stream, { String prefix = '', List<Pattern> filter = const <Pattern>[] }) {
|
|
assert(prefix != null);
|
|
assert(filter != null);
|
|
stream
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen((String line) {
|
|
if (!filter.any((Pattern pattern) => line.contains(pattern)))
|
|
print('$prefix$line'.trim());
|
|
});
|
|
}
|