Unverified Commit f9c6f305 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Allow users to create samples using flutter create. (#23584)

This adds flutter create --sample which allows users to execute a command which will create a working sample app from samples embedded in the API docs.

The command looks something like this:

flutter create --sample=chip.DeletableChipAttributes.onDeleted mysample
parent 4559ae1a
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
</div> </div>
<div class="snippet" id="longSnippet" hidden> <div class="snippet" id="longSnippet" hidden>
<div class="snippet-description">To create a sample project with this code snippet, run:<br/> <div class="snippet-description">To create a sample project with this code snippet, run:<br/>
<span class="snippet-create-command">flutter create --snippet={{id}} mysample</span> <span class="snippet-create-command">flutter create --sample={{id}} mysample</span>
</div> </div>
<div class="copyable-container"> <div class="copyable-container">
<button class="copy-button-overlay copy-button" title="Copy to clipboard" <button class="copy-button-overlay copy-button" title="Copy to clipboard"
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:linter/src/rules/pub/package_names.dart' as package_names; // ignore: implementation_imports import 'package:linter/src/rules/pub/package_names.dart' as package_names; // ignore: implementation_imports
import 'package:linter/src/utils.dart' as linter_utils; // ignore: implementation_imports import 'package:linter/src/utils.dart' as linter_utils; // ignore: implementation_imports
...@@ -13,6 +14,7 @@ import '../android/android_sdk.dart' as android_sdk; ...@@ -13,6 +14,7 @@ import '../android/android_sdk.dart' as android_sdk;
import '../android/gradle.dart' as gradle; import '../android/gradle.dart' as gradle;
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/net.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../cache.dart'; import '../cache.dart';
...@@ -84,6 +86,20 @@ class CreateCommand extends FlutterCommand { ...@@ -84,6 +86,20 @@ class CreateCommand extends FlutterCommand {
}, },
defaultsTo: null, defaultsTo: null,
); );
argParser.addOption(
'sample',
abbr: 's',
help: 'Specifies the Flutter code sample to use as the main.dart for an application. Implies '
'--template=app.',
defaultsTo: null,
valueHelp: 'the sample ID of the desired sample from the API documentation website (http://docs.flutter.io)'
);
argParser.addFlag(
'overwrite',
negatable: true,
defaultsTo: false,
help: 'When performing operations, overwrite existing files.',
);
argParser.addOption( argParser.addOption(
'description', 'description',
defaultsTo: 'A new Flutter project.', defaultsTo: 'A new Flutter project.',
...@@ -162,6 +178,19 @@ class CreateCommand extends FlutterCommand { ...@@ -162,6 +178,19 @@ class CreateCommand extends FlutterCommand {
return null; return null;
} }
Future<String> _fetchSampleFromServer(String sampleId) async {
// Sanity check the sampleId
if (sampleId.contains(RegExp(r'[^-\w\.]'))) {
throwToolExit('Sample ID "$sampleId" contains invalid characters. Check the ID in the '
'documentation and try again.');
}
final String host = FlutterVersion.instance.channel == 'stable'
? 'docs.flutter.io'
: 'master-docs-flutter-io.firebaseapp.com';
return utf8.decode(await fetchUrl(Uri.https(host, 'snippets/$sampleId.dart')));
}
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
if (argResults.rest.isEmpty) if (argResults.rest.isEmpty)
...@@ -198,6 +227,17 @@ class CreateCommand extends FlutterCommand { ...@@ -198,6 +227,17 @@ class CreateCommand extends FlutterCommand {
final Directory projectDir = fs.directory(argResults.rest.first); final Directory projectDir = fs.directory(argResults.rest.first);
final String projectDirPath = fs.path.normalize(projectDir.absolute.path); final String projectDirPath = fs.path.normalize(projectDir.absolute.path);
String sampleCode;
if (argResults['sample'] != null) {
if (argResults['template'] != null &&
_stringToProjectType(argResults['template'] ?? 'app') != _ProjectType.app) {
throwToolExit('Cannot specify --sample with a project type other than '
'"${getEnumName(_ProjectType.app)}"');
}
// Fetch the sample from the server.
sampleCode = await _fetchSampleFromServer(argResults['sample']);
}
_ProjectType template; _ProjectType template;
_ProjectType detectedProjectType; _ProjectType detectedProjectType;
final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync(); final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
...@@ -241,7 +281,7 @@ class CreateCommand extends FlutterCommand { ...@@ -241,7 +281,7 @@ class CreateCommand extends FlutterCommand {
} }
} }
String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot); String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot, overwrite: argResults['overwrite']);
if (error != null) if (error != null)
throwToolExit(error); throwToolExit(error);
...@@ -262,29 +302,38 @@ class CreateCommand extends FlutterCommand { ...@@ -262,29 +302,38 @@ class CreateCommand extends FlutterCommand {
); );
final String relativeDirPath = fs.path.relative(projectDirPath); final String relativeDirPath = fs.path.relative(projectDirPath);
if (!projectDir.existsSync()) { if (!projectDir.existsSync() || projectDir.listSync().isEmpty) {
printStatus('Creating project $relativeDirPath...'); printStatus('Creating project $relativeDirPath...');
} else { } else {
if (sampleCode != null && !argResults['overwrite']) {
throwToolExit('Will not overwrite existing project in $relativeDirPath: '
'must specify --overwrite for samples to overwrite.');
}
printStatus('Recreating project $relativeDirPath...'); printStatus('Recreating project $relativeDirPath...');
} }
final Directory relativeDir = fs.directory(projectDirPath); final Directory relativeDir = fs.directory(projectDirPath);
int generatedFileCount = 0; int generatedFileCount = 0;
switch (template) { switch (template) {
case _ProjectType.app: case _ProjectType.app:
generatedFileCount += await _generateApp(relativeDir, templateContext); generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: argResults['overwrite']);
break; break;
case _ProjectType.module: case _ProjectType.module:
generatedFileCount += await _generateModule(relativeDir, templateContext); generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: argResults['overwrite']);
break; break;
case _ProjectType.package: case _ProjectType.package:
generatedFileCount += await _generatePackage(relativeDir, templateContext); generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: argResults['overwrite']);
break; break;
case _ProjectType.plugin: case _ProjectType.plugin:
generatedFileCount += await _generatePlugin(relativeDir, templateContext); generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: argResults['overwrite']);
break; break;
} }
if (sampleCode != null) {
generatedFileCount += await _applySample(relativeDir, sampleCode);
}
printStatus('Wrote $generatedFileCount files.'); printStatus('Wrote $generatedFileCount files.');
printStatus('\nAll done!'); printStatus('\nAll done!');
final String application = sampleCode != null ? 'sample application' : 'application';
if (generatePackage) { if (generatePackage) {
final String relativeMainPath = fs.path.normalize(fs.path.join( final String relativeMainPath = fs.path.normalize(fs.path.join(
relativeDirPath, relativeDirPath,
...@@ -312,12 +361,12 @@ class CreateCommand extends FlutterCommand { ...@@ -312,12 +361,12 @@ class CreateCommand extends FlutterCommand {
await doctor.summary(); await doctor.summary();
printStatus(''' printStatus('''
In order to run your application, type: In order to run your $application, type:
\$ cd $relativeAppPath \$ cd $relativeAppPath
\$ flutter run \$ flutter run
Your application code is in $relativeAppMain. Your $application code is in $relativeAppMain.
'''); ''');
if (generatePlugin) { if (generatePlugin) {
printStatus(''' printStatus('''
...@@ -339,20 +388,20 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit ...@@ -339,20 +388,20 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit
're-validate your setup.'); 're-validate your setup.');
printStatus("When complete, type 'flutter run' from the '$relativeAppPath' " printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
'directory in order to launch your app.'); 'directory in order to launch your app.');
printStatus('Your application code is in $relativeAppMain'); printStatus('Your $application code is in $relativeAppMain');
} }
} }
return null; return null;
} }
Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext) async { Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
int generatedCount = 0; int generatedCount = 0;
final String description = argResults.wasParsed('description') final String description = argResults.wasParsed('description')
? argResults['description'] ? argResults['description']
: 'A new flutter module project.'; : 'A new flutter module project.';
templateContext['description'] = description; templateContext['description'] = description;
generatedCount += _renderTemplate(fs.path.join('module', 'common'), directory, templateContext); generatedCount += _renderTemplate(fs.path.join('module', 'common'), directory, templateContext, overwrite: overwrite);
if (argResults['pub']) { if (argResults['pub']) {
await pubGet( await pubGet(
context: PubContext.create, context: PubContext.create,
...@@ -365,13 +414,13 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit ...@@ -365,13 +414,13 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit
return generatedCount; return generatedCount;
} }
Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext) async { Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
int generatedCount = 0; int generatedCount = 0;
final String description = argResults.wasParsed('description') final String description = argResults.wasParsed('description')
? argResults['description'] ? argResults['description']
: 'A new Flutter package project.'; : 'A new Flutter package project.';
templateContext['description'] = description; templateContext['description'] = description;
generatedCount += _renderTemplate('package', directory, templateContext); generatedCount += _renderTemplate('package', directory, templateContext, overwrite: overwrite);
if (argResults['pub']) { if (argResults['pub']) {
await pubGet( await pubGet(
context: PubContext.createPackage, context: PubContext.createPackage,
...@@ -382,13 +431,13 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit ...@@ -382,13 +431,13 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit
return generatedCount; return generatedCount;
} }
Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext) async { Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
int generatedCount = 0; int generatedCount = 0;
final String description = argResults.wasParsed('description') final String description = argResults.wasParsed('description')
? argResults['description'] ? argResults['description']
: 'A new flutter plugin project.'; : 'A new flutter plugin project.';
templateContext['description'] = description; templateContext['description'] = description;
generatedCount += _renderTemplate('plugin', directory, templateContext); generatedCount += _renderTemplate('plugin', directory, templateContext, overwrite: overwrite);
if (argResults['pub']) { if (argResults['pub']) {
await pubGet( await pubGet(
context: PubContext.createPlugin, context: PubContext.createPlugin,
...@@ -410,19 +459,19 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit ...@@ -410,19 +459,19 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit
templateContext['pluginProjectName'] = projectName; templateContext['pluginProjectName'] = projectName;
templateContext['androidPluginIdentifier'] = androidPluginIdentifier; templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
generatedCount += await _generateApp(project.example.directory, templateContext); generatedCount += await _generateApp(project.example.directory, templateContext, overwrite: overwrite);
return generatedCount; return generatedCount;
} }
Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext) async { Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
int generatedCount = 0; int generatedCount = 0;
generatedCount += _renderTemplate('app', directory, templateContext); generatedCount += _renderTemplate('app', directory, templateContext, overwrite: overwrite);
final FlutterProject project = await FlutterProject.fromDirectory(directory); final FlutterProject project = await FlutterProject.fromDirectory(directory);
generatedCount += _injectGradleWrapper(project); generatedCount += _injectGradleWrapper(project);
if (argResults['with-driver-test']) { if (argResults['with-driver-test']) {
final Directory testDirectory = directory.childDirectory('test_driver'); final Directory testDirectory = directory.childDirectory('test_driver');
generatedCount += _renderTemplate('driver', testDirectory, templateContext); generatedCount += _renderTemplate('driver', testDirectory, templateContext, overwrite: overwrite);
} }
if (argResults['pub']) { if (argResults['pub']) {
...@@ -435,6 +484,20 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit ...@@ -435,6 +484,20 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit
return generatedCount; return generatedCount;
} }
// Takes an application template and replaces the main.dart with one from the
// documentation website in sampleCode. Returns the difference in the number
// of files after applying the sample, since it also deletes the application's
// test directory (since the template's test doesn't apply to the sample).
Future<int> _applySample(Directory directory, String sampleCode) async {
final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
await mainDartFile.create(recursive: true);
await mainDartFile.writeAsString(sampleCode);
final Directory testDir = directory.childDirectory('test');
final List<FileSystemEntity> files = testDir.listSync(recursive: true);
await testDir.delete(recursive: true);
return -files.length;
}
Map<String, dynamic> _templateContext({ Map<String, dynamic> _templateContext({
String organization, String organization,
String projectName, String projectName,
...@@ -473,9 +536,9 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit ...@@ -473,9 +536,9 @@ To edit platform code in an IDE see https://flutter.io/developing-packages/#edit
}; };
} }
int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context) { int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context, {bool overwrite = false}) {
final Template template = Template.fromName(templateName); final Template template = Template.fromName(templateName);
return template.render(directory, context, overwriteExisting: false); return template.render(directory, context, overwriteExisting: overwrite);
} }
int _injectGradleWrapper(FlutterProject project) { int _injectGradleWrapper(FlutterProject project) {
...@@ -557,12 +620,22 @@ String _validateProjectName(String projectName) { ...@@ -557,12 +620,22 @@ String _validateProjectName(String projectName) {
/// Return null if the project directory is legal. Return a validation message /// Return null if the project directory is legal. Return a validation message
/// if we should disallow the directory name. /// if we should disallow the directory name.
String _validateProjectDir(String dirPath, { String flutterRoot }) { String _validateProjectDir(String dirPath, { String flutterRoot, bool overwrite = false }) {
if (fs.path.isWithin(flutterRoot, dirPath)) { if (fs.path.isWithin(flutterRoot, dirPath)) {
return 'Cannot create a project within the Flutter SDK. ' return 'Cannot create a project within the Flutter SDK. '
"Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'."; "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
} }
// If the destination directory is actually a file, then we refuse to
// overwrite, on the theory that the user probably didn't expect it to exist.
if (fs.isFileSync(dirPath)) {
return "Invalid project name: '$dirPath' - refers to an existing file."
'${overwrite ? ' Refusing to overwrite a file with a directory.' : ''}';
}
if (overwrite)
return null;
final FileSystemEntityType type = fs.typeSync(dirPath); final FileSystemEntityType type = fs.typeSync(dirPath);
if (type != FileSystemEntityType.notFound) { if (type != FileSystemEntityType.notFound) {
......
...@@ -19,7 +19,7 @@ import 'globals.dart'; ...@@ -19,7 +19,7 @@ import 'globals.dart';
class FlutterVersion { class FlutterVersion {
@visibleForTesting @visibleForTesting
FlutterVersion(this._clock) { FlutterVersion([this._clock = const Clock()]) {
_channel = _runGit('git rev-parse --abbrev-ref --symbolic @{u}'); _channel = _runGit('git rev-parse --abbrev-ref --symbolic @{u}');
final String branch = _runGit('git rev-parse --abbrev-ref HEAD'); final String branch = _runGit('git rev-parse --abbrev-ref HEAD');
_branch = branch == 'HEAD' ? _channel : branch; _branch = branch == 'HEAD' ? _channel : branch;
......
...@@ -8,6 +8,7 @@ import 'dart:convert'; ...@@ -8,6 +8,7 @@ import 'dart:convert';
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/net.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/create.dart'; import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/dart/sdk.dart'; import 'package:flutter_tools/src/dart/sdk.dart';
...@@ -56,6 +57,7 @@ void main() { ...@@ -56,6 +57,7 @@ void main() {
'ios/Flutter/AppFrameworkInfo.plist', 'ios/Flutter/AppFrameworkInfo.plist',
'ios/Runner/AppDelegate.m', 'ios/Runner/AppDelegate.m',
'ios/Runner/GeneratedPluginRegistrant.h', 'ios/Runner/GeneratedPluginRegistrant.h',
'lib/main.dart',
], ],
); );
return _runFlutterTest(projectDir); return _runFlutterTest(projectDir);
...@@ -719,18 +721,52 @@ void main() { ...@@ -719,18 +721,52 @@ void main() {
); );
}); });
// Verify that we fail with an error code when the file exists. testUsingContext('fails when file exists where output directory should be', () async {
testUsingContext('fails when file exists', () async {
Cache.flutterRoot = '../..'; Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand(); final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command); final CommandRunner<void> runner = createTestCommandRunner(command);
final File existingFile = fs.file('${projectDir.path.toString()}/bad'); final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad'));
if (!existingFile.existsSync()) { if (!existingFile.existsSync()) {
existingFile.createSync(recursive: true); existingFile.createSync(recursive: true);
} }
expect( expect(
runner.run(<String>['create', existingFile.path]), runner.run(<String>['create', existingFile.path]),
throwsToolExit(message: 'file exists'), throwsToolExit(message: 'existing file'),
);
});
testUsingContext('fails overwrite when file exists where output directory should be', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad'));
if (!existingFile.existsSync()) {
existingFile.createSync(recursive: true);
}
expect(
runner.run(<String>['create', '--overwrite', existingFile.path]),
throwsToolExit(message: 'existing file'),
);
});
testUsingContext('overwrites existing directory when requested', () async {
Cache.flutterRoot = '../..';
final Directory existingDirectory = fs.directory(fs.path.join(projectDir.path, 'bad'));
if (!existingDirectory.existsSync()) {
existingDirectory.createSync(recursive: true);
}
final File existingFile = fs.file(fs.path.join(existingDirectory.path, 'lib', 'main.dart'));
existingFile.createSync(recursive: true);
await _createProject(
fs.directory(existingDirectory.path),
<String>['--overwrite'],
<String>[
'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
'lib/main.dart',
'ios/Flutter/AppFrameworkInfo.plist',
'ios/Runner/AppDelegate.m',
'ios/Runner/GeneratedPluginRegistrant.h',
],
); );
}); });
...@@ -779,6 +815,24 @@ void main() { ...@@ -779,6 +815,24 @@ void main() {
ProcessManager: () => loggingProcessManager, ProcessManager: () => loggingProcessManager,
}, },
); );
testUsingContext('can create a sample-based project', () async {
await _createAndAnalyzeProject(
projectDir,
<String>['--no-pub', '--sample=foo.bar.Baz'],
<String>[
'lib/main.dart',
'flutter_project.iml',
'android/app/src/main/AndroidManifest.xml',
'ios/Flutter/AppFrameworkInfo.plist',
],
unexpectedPaths: <String>['test'],
);
expect(projectDir.childDirectory('lib').childFile('main.dart').readAsStringSync(),
contains('void main() {}'));
}, timeout: allowForRemotePubInvocation, overrides: <Type, Generator>{
HttpClientFactory: () => () => MockHttpClient(200, result: 'void main() {}'),
});
} }
Future<void> _createProject( Future<void> _createProject(
...@@ -901,3 +955,62 @@ class LoggingProcessManager extends LocalProcessManager { ...@@ -901,3 +955,62 @@ class LoggingProcessManager extends LocalProcessManager {
); );
} }
} }
class MockHttpClient implements HttpClient {
MockHttpClient(this.statusCode, {this.result});
final int statusCode;
final String result;
@override
Future<HttpClientRequest> getUrl(Uri url) async {
return MockHttpClientRequest(statusCode, result: result);
}
@override
dynamic noSuchMethod(Invocation invocation) {
throw 'io.HttpClient - $invocation';
}
}
class MockHttpClientRequest implements HttpClientRequest {
MockHttpClientRequest(this.statusCode, {this.result});
final int statusCode;
final String result;
@override
Future<HttpClientResponse> close() async {
return MockHttpClientResponse(statusCode, result: result);
}
@override
dynamic noSuchMethod(Invocation invocation) {
throw 'io.HttpClientRequest - $invocation';
}
}
class MockHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
MockHttpClientResponse(this.statusCode, {this.result});
@override
final int statusCode;
final String result;
@override
String get reasonPhrase => '<reason phrase>';
@override
StreamSubscription<List<int>> listen(void onData(List<int> event), {
Function onError, void onDone(), bool cancelOnError
}) {
return Stream<List<int>>.fromIterable(<List<int>>[result.codeUnits])
.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
@override
dynamic noSuchMethod(Invocation invocation) {
throw 'io.HttpClientResponse - $invocation';
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment