create.dart 25.5 KB
Newer Older
1 2 3 4 5
// Copyright 2015 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';
6
import 'dart:convert';
7

8
import 'package:linter/src/rules/pub/package_names.dart' as package_names; // ignore: implementation_imports
9
import 'package:linter/src/utils.dart' as linter_utils; // ignore: implementation_imports
10
import 'package:yaml/yaml.dart' as yaml;
11

12
import '../android/android.dart' as android;
13
import '../android/android_sdk.dart' as android_sdk;
14
import '../android/gradle.dart' as gradle;
15
import '../base/common.dart';
16
import '../base/file_system.dart';
17
import '../base/net.dart';
18
import '../base/os.dart';
19
import '../base/utils.dart';
20
import '../cache.dart';
21
import '../dart/pub.dart';
22
import '../doctor.dart';
23
import '../globals.dart';
24
import '../project.dart';
25
import '../runner/flutter_command.dart';
26
import '../template.dart';
27
import '../version.dart';
Hixie's avatar
Hixie committed
28

29
enum _ProjectType {
30 31 32
  /// This is the default project with the user-managed host code.
  /// It is different than the "module" template in that it exposes and doesn't
  /// manage the platform code.
33
  app,
34
  /// The is a project that has managed platform host code. It is an application with
35
  /// ephemeral .ios and .android directories that can be updated automatically.
36
  module,
37 38
  /// This is a Flutter Dart package project. It doesn't have any native
  /// components, only Dart.
39
  package,
40
  /// This is a native plugin project.
41 42 43
  plugin,
}

44 45 46
_ProjectType _stringToProjectType(String value) {
  _ProjectType result;
  for (_ProjectType type in _ProjectType.values) {
47 48 49 50 51 52 53 54
    if (value == getEnumName(type)) {
      result = type;
      break;
    }
  }
  return result;
}

55
class CreateCommand extends FlutterCommand {
56
  CreateCommand() {
57 58 59 60
    argParser.addFlag('pub',
      defaultsTo: true,
      help: 'Whether to run "flutter packages get" after the project has been created.'
    );
61 62 63 64 65 66
    argParser.addFlag('offline',
      defaultsTo: false,
      help: 'When "flutter packages get" is run by the create command, this indicates '
        'whether to run it in offline mode or not. In offline mode, it will need to '
        'have all dependencies already available in the pub cache to succeed.'
    );
67 68 69 70
    argParser.addFlag(
      'with-driver-test',
      negatable: true,
      defaultsTo: false,
71
      help: "Also add a flutter_driver dependency and generate a sample 'flutter drive' test."
72
    );
73 74 75
    argParser.addOption(
      'template',
      abbr: 't',
76
      allowed: _ProjectType.values.map<String>((_ProjectType type) => getEnumName(type)),
77 78 79
      help: 'Specify the type of project to create.',
      valueHelp: 'type',
      allowedHelp: <String, String>{
80
        getEnumName(_ProjectType.app): '(default) Generate a Flutter application.',
81
        getEnumName(_ProjectType.package): 'Generate a shareable Flutter project containing modular '
82
            'Dart code.',
83
        getEnumName(_ProjectType.plugin): 'Generate a shareable Flutter project containing an API '
84 85
            'in Dart code with a platform-specific implementation for Android, for iOS code, or '
            'for both.',
86
      },
87
      defaultsTo: null,
88
    );
89 90 91 92 93 94 95 96 97 98 99 100 101 102
    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.',
    );
103 104
    argParser.addOption(
      'description',
105 106 107 108 109
      defaultsTo: 'A new Flutter project.',
      help: 'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.'
    );
    argParser.addOption(
      'org',
110
      defaultsTo: 'com.example',
111
      help: 'The organization responsible for your new Flutter project, in reverse domain name notation. '
112
            'This string is used in Java package names and as prefix in the iOS bundle identifier.'
113
    );
114 115 116 117 118
    argParser.addOption(
      'project-name',
      defaultsTo: null,
      help: 'The project name for this new Flutter project. This must be a valid dart package name.'
    );
119 120 121 122 123 124 125 126 127 128 129 130
    argParser.addOption(
      'ios-language',
      abbr: 'i',
      defaultsTo: 'objc',
      allowed: <String>['objc', 'swift'],
    );
    argParser.addOption(
      'android-language',
      abbr: 'a',
      defaultsTo: 'java',
      allowed: <String>['java', 'kotlin'],
    );
131 132
  }

133 134 135 136 137 138 139
  @override
  final String name = 'create';

  @override
  final String description = 'Create a new Flutter project.\n\n'
    'If run on a project that already exists, this will repair the project, recreating any files that are missing.';

140
  @override
141
  String get invocation => '${runner.executableName} $name <output directory>';
142

143 144
  // If it has a .metadata file with the project_type in it, use that.
  // If it has an android dir and an android/app dir, it's a legacy app
145 146 147 148
  // If it has an ios dir and an ios/Flutter dir, it's a legacy app
  // Otherwise, we don't presume to know what type of project it could be, since
  // many of the files could be missing, and we can't really tell definitively.
  _ProjectType _determineTemplateType(Directory projectDir) {
149 150 151
    yaml.YamlMap loadMetadata(Directory projectDir) {
      if (!projectDir.existsSync())
        return null;
152
      final File metadataFile = fs.file(fs.path.join(projectDir.absolute.path, '.metadata'));
153 154 155 156 157
      if (!metadataFile.existsSync())
        return null;
      return yaml.loadYaml(metadataFile.readAsStringSync());
    }

158 159 160 161
    bool exists(List<String> path) {
      return fs.directory(fs.path.joinAll(<String>[projectDir.absolute.path] + path)).existsSync();
    }

162 163 164 165 166 167 168 169 170
    // If it exists, the project type in the metadata is definitive.
    final yaml.YamlMap metadata = loadMetadata(projectDir);
    if (metadata != null && metadata['project_type'] != null) {
      return _stringToProjectType(metadata['project_type']);
    }

    // There either wasn't any metadata, or it didn't contain the project type,
    // so try and figure out what type of project it is from the existing
    // directory structure.
171 172 173 174 175 176 177
    if (exists(<String>['android', 'app'])
        || exists(<String>['ios', 'Runner'])
        || exists(<String>['ios', 'Flutter'])) {
      return _ProjectType.app;
    }
    // Since we can't really be definitive on nearly-empty directories, err on
    // the side of prudence and just say we don't know.
178 179 180
    return null;
  }

181 182 183 184 185 186 187 188 189 190 191 192 193
  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')));
  }

194
  @override
195
  Future<FlutterCommandResult> runCommand() async {
196 197
    if (argResults.rest.isEmpty)
      throwToolExit('No option specified for the output directory.\n$usage', exitCode: 2);
198

199
    if (argResults.rest.length > 1) {
200
      String message = 'Multiple output directories specified.';
201 202
      for (String arg in argResults.rest) {
        if (arg.startsWith('-')) {
203
          message += '\nTry moving $arg to be immediately following $name';
204 205 206
          break;
        }
      }
207
      throwToolExit(message, exitCode: 2);
208 209
    }

210
    if (Cache.flutterRoot == null)
211
      throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment '
212
        'variable was specified. Unable to find package:flutter.', exitCode: 2);
213

214 215
    await Cache.instance.updateAll();

216
    final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
217

218 219
    final String flutterPackagesDirectory = fs.path.join(flutterRoot, 'packages');
    final String flutterPackagePath = fs.path.join(flutterPackagesDirectory, 'flutter');
220
    if (!fs.isFileSync(fs.path.join(flutterPackagePath, 'pubspec.yaml')))
221
      throwToolExit('Unable to find package:flutter in $flutterPackagePath', exitCode: 2);
222

223
    final String flutterDriverPackagePath = fs.path.join(flutterRoot, 'packages', 'flutter_driver');
224
    if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml')))
225
      throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
226

227
    final Directory projectDir = fs.directory(argResults.rest.first);
228
    final String projectDirPath = fs.path.normalize(projectDir.absolute.path);
229

230 231 232 233 234 235 236 237 238 239 240
    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']);
    }

241 242 243
    _ProjectType template;
    _ProjectType detectedProjectType;
    final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
244 245
    if (argResults['template'] != null) {
      template = _stringToProjectType(argResults['template']);
246 247 248 249 250 251 252 253 254 255 256
    } else {
      if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
        detectedProjectType = _determineTemplateType(projectDir);
        if (detectedProjectType == null && metadataExists) {
          // We can only be definitive that this is the wrong type if the .metadata file
          // exists and contains a type that we don't understand, or doesn't contain a type.
          throwToolExit('Sorry, unable to detect the type of project to recreate. '
              'Try creating a fresh project and migrating your existing code to '
              'the new project manually.');
        }
      }
257
    }
258
    template ??= detectedProjectType ?? _ProjectType.app;
259 260 261
    if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
      // We can only be definitive that this is the wrong type if the .metadata file
      // exists and contains a type that doesn't match.
262 263 264 265
      throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the "
          "existing template type of '${getEnumName(detectedProjectType)}'.");
    }

266
    final bool generateModule = template == _ProjectType.module;
267 268
    final bool generatePlugin = template == _ProjectType.plugin;
    final bool generatePackage = template == _ProjectType.package;
269

270 271
    String organization = argResults['org'];
    if (!argResults.wasParsed('org')) {
272
      final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
273
      final Set<String> existingOrganizations = project.organizationNames;
274 275 276 277
      if (existingOrganizations.length == 1) {
        organization = existingOrganizations.first;
      } else if (1 < existingOrganizations.length) {
        throwToolExit(
278
          'Ambiguous organization in existing files: $existingOrganizations. '
279 280 281 282
          'The --org command line argument must be specified to recreate project.'
        );
      }
    }
283

284
    String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot, overwrite: argResults['overwrite']);
285 286
    if (error != null)
      throwToolExit(error);
Devon Carew's avatar
Devon Carew committed
287

288
    final String projectName = argResults['project-name'] ?? fs.path.basename(projectDirPath);
289
    error = _validateProjectName(projectName);
290 291
    if (error != null)
      throwToolExit(error);
292

293
    final Map<String, dynamic> templateContext = _templateContext(
294
      organization: organization,
295 296 297 298 299 300 301
      projectName: projectName,
      projectDescription: argResults['description'],
      flutterRoot: flutterRoot,
      renderDriverTest: argResults['with-driver-test'],
      withPluginHook: generatePlugin,
      androidLanguage: argResults['android-language'],
      iosLanguage: argResults['ios-language'],
302
    );
303

304
    final String relativeDirPath = fs.path.relative(projectDirPath);
305
    if (!projectDir.existsSync() || projectDir.listSync().isEmpty) {
306 307
      printStatus('Creating project $relativeDirPath...');
    } else {
308 309 310 311
      if (sampleCode != null && !argResults['overwrite']) {
        throwToolExit('Will not overwrite existing project in $relativeDirPath: '
          'must specify --overwrite for samples to overwrite.');
      }
312 313
      printStatus('Recreating project $relativeDirPath...');
    }
314

315
    final Directory relativeDir = fs.directory(projectDirPath);
316 317
    int generatedFileCount = 0;
    switch (template) {
318
      case _ProjectType.app:
319
        generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: argResults['overwrite']);
320
        break;
321
      case _ProjectType.module:
322
        generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: argResults['overwrite']);
323
        break;
324
      case _ProjectType.package:
325
        generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: argResults['overwrite']);
326
        break;
327
      case _ProjectType.plugin:
328
        generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: argResults['overwrite']);
329 330
        break;
    }
331 332 333
    if (sampleCode != null) {
      generatedFileCount += await _applySample(relativeDir, sampleCode);
    }
334
    printStatus('Wrote $generatedFileCount files.');
335
    printStatus('\nAll done!');
336
    final String application = sampleCode != null ? 'sample application' : 'application';
337
    if (generatePackage) {
338 339 340 341 342
      final String relativeMainPath = fs.path.normalize(fs.path.join(
        relativeDirPath,
        'lib',
        '${templateContext['projectName']}.dart',
      ));
343
      printStatus('Your package code is in $relativeMainPath');
344
    } else if (generateModule) {
345 346 347 348 349
      final String relativeMainPath = fs.path.normalize(fs.path.join(
          relativeDirPath,
          'lib',
          'main.dart',
      ));
350
      printStatus('Your module code is in $relativeMainPath.');
351 352
    } else {
      // Run doctor; tell the user the next steps.
353
      final FlutterProject project = await FlutterProject.fromPath(projectDirPath);
354
      final FlutterProject app = project.hasExampleApp ? project.example : project;
355 356
      final String relativeAppPath = fs.path.normalize(fs.path.relative(app.directory.path));
      final String relativeAppMain = fs.path.join(relativeAppPath, 'lib', 'main.dart');
357
      final String relativePluginPath = fs.path.normalize(fs.path.relative(projectDirPath));
358
      final String relativePluginMain = fs.path.join(relativePluginPath, 'lib', '$projectName.dart');
359 360 361 362 363
      if (doctor.canLaunchAnything) {
        // Let them know a summary of the state of their tooling.
        await doctor.summary();

        printStatus('''
364
In order to run your $application, type:
365 366 367 368

  \$ cd $relativeAppPath
  \$ flutter run

369
Your $application code is in $relativeAppMain.
370 371 372
''');
        if (generatePlugin) {
          printStatus('''
373
Your plugin code is in $relativePluginMain.
374

375
Host platform code is in the "android" and "ios" directories under $relativePluginPath.
376
To edit platform code in an IDE see https://flutter.io/developing-packages/#edit-plugin-package.
377 378 379 380 381 382 383 384 385 386 387 388 389 390
''');
        }
      } else {
        printStatus("You'll need to install additional components before you can run "
            'your Flutter app:');
        printStatus('');

        // Give the user more detailed analysis.
        await doctor.diagnose();
        printStatus('');
        printStatus("After installing components, run 'flutter doctor' in order to "
            're-validate your setup.');
        printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
            'directory in order to launch your app.');
391
        printStatus('Your $application code is in $relativeAppMain');
392
      }
393
    }
394 395

    return null;
396
  }
397

398
  Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
399 400 401
    int generatedCount = 0;
    final String description = argResults.wasParsed('description')
        ? argResults['description']
402
        : 'A new flutter module project.';
403
    templateContext['description'] = description;
404
    generatedCount += _renderTemplate(fs.path.join('module', 'common'), directory, templateContext, overwrite: overwrite);
405 406 407
    if (argResults['pub']) {
      await pubGet(
        context: PubContext.create,
408
        directory: directory.path,
409 410
        offline: argResults['offline'],
      );
411
      final FlutterProject project = await FlutterProject.fromDirectory(directory);
412 413 414 415 416
      await project.ensureReadyForPlatformSpecificTooling();
    }
    return generatedCount;
  }

417
  Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
418 419
    int generatedCount = 0;
    final String description = argResults.wasParsed('description')
420
        ? argResults['description']
421
        : 'A new Flutter package project.';
422
    templateContext['description'] = description;
423
    generatedCount += _renderTemplate('package', directory, templateContext, overwrite: overwrite);
424 425 426
    if (argResults['pub']) {
      await pubGet(
        context: PubContext.createPackage,
427
        directory: directory.path,
428 429
        offline: argResults['offline'],
      );
430
    }
431 432
    return generatedCount;
  }
433

434
  Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
435 436 437 438 439
    int generatedCount = 0;
    final String description = argResults.wasParsed('description')
        ? argResults['description']
        : 'A new flutter plugin project.';
    templateContext['description'] = description;
440
    generatedCount += _renderTemplate('plugin', directory, templateContext, overwrite: overwrite);
441 442 443
    if (argResults['pub']) {
      await pubGet(
        context: PubContext.createPlugin,
444
        directory: directory.path,
445 446 447
        offline: argResults['offline'],
      );
    }
448
    final FlutterProject project = await FlutterProject.fromDirectory(directory);
449
    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
450 451 452 453 454 455 456 457 458 459 460 461

    final String projectName = templateContext['projectName'];
    final String organization = templateContext['organization'];
    final String androidPluginIdentifier = templateContext['androidIdentifier'];
    final String exampleProjectName = projectName + '_example';
    templateContext['projectName'] = exampleProjectName;
    templateContext['androidIdentifier'] = _createAndroidIdentifier(organization, exampleProjectName);
    templateContext['iosIdentifier'] = _createUTIIdentifier(organization, exampleProjectName);
    templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
    templateContext['pluginProjectName'] = projectName;
    templateContext['androidPluginIdentifier'] = androidPluginIdentifier;

462
    generatedCount += await _generateApp(project.example.directory, templateContext, overwrite: overwrite);
463 464 465
    return generatedCount;
  }

466
  Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext, {bool overwrite = false}) async {
467
    int generatedCount = 0;
468
    generatedCount += _renderTemplate('app', directory, templateContext, overwrite: overwrite);
469
    final FlutterProject project = await FlutterProject.fromDirectory(directory);
470
    generatedCount += _injectGradleWrapper(project);
471

472
    if (argResults['with-driver-test']) {
473
      final Directory testDirectory = directory.childDirectory('test_driver');
474
      generatedCount += _renderTemplate('driver', testDirectory, templateContext, overwrite: overwrite);
475 476
    }

477
    if (argResults['pub']) {
478
      await pubGet(context: PubContext.create, directory: directory.path, offline: argResults['offline']);
479
      await project.ensureReadyForPlatformSpecificTooling();
480
    }
Hixie's avatar
Hixie committed
481

482
    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
483

484
    return generatedCount;
Hixie's avatar
Hixie committed
485
  }
486

487 488 489 490 491 492 493 494 495 496 497 498 499 500
  // 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;
  }

501
  Map<String, dynamic> _templateContext({
502
    String organization,
503 504 505 506 507
    String projectName,
    String projectDescription,
    String androidLanguage,
    String iosLanguage,
    String flutterRoot,
508 509
    bool renderDriverTest = false,
    bool withPluginHook = false,
510
  }) {
511
    flutterRoot = fs.path.normalize(flutterRoot);
512

513 514 515 516
    final String pluginDartClass = _createPluginClassName(projectName);
    final String pluginClass = pluginDartClass.endsWith('Plugin')
        ? pluginDartClass
        : pluginDartClass + 'Plugin';
517

518
    return <String, dynamic>{
519
      'organization': organization,
520
      'projectName': projectName,
521 522
      'androidIdentifier': _createAndroidIdentifier(organization, projectName),
      'iosIdentifier': _createUTIIdentifier(organization, projectName),
523
      'description': projectDescription,
524
      'dartSdk': '$flutterRoot/bin/cache/dart-sdk',
525
      'androidMinApiLevel': android.minApiLevel,
526
      'androidSdkVersion': android_sdk.minimumAndroidSdkVersion,
527
      'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar',
528 529 530 531
      'withDriverTest': renderDriverTest,
      'pluginClass': pluginClass,
      'pluginDartClass': pluginDartClass,
      'withPluginHook': withPluginHook,
532 533
      'androidLanguage': androidLanguage,
      'iosLanguage': iosLanguage,
534 535
      'flutterRevision': FlutterVersion.instance.frameworkRevision,
      'flutterChannel': FlutterVersion.instance.channel,
536
    };
537
  }
538

539
  int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context, {bool overwrite = false}) {
540
    final Template template = Template.fromName(templateName);
541
    return template.render(directory, context, overwriteExisting: overwrite);
542
  }
543

544
  int _injectGradleWrapper(FlutterProject project) {
545 546 547
    int filesCreated = 0;
    copyDirectorySync(
      cache.getArtifactDirectory('gradle_wrapper'),
548
      project.android.hostAppGradleRoot,
549 550 551 552 553 554 555 556 557 558
      (File sourceFile, File destinationFile) {
        filesCreated++;
        final String modes = sourceFile.statSync().modeString();
        if (modes != null && modes.contains('x')) {
          os.makeExecutable(destinationFile);
        }
      },
    );
    return filesCreated;
  }
559 560
}

561
String _createAndroidIdentifier(String organization, String name) {
562
  return '$organization.$name'.replaceAll('_', '');
563 564 565 566 567
}

String _createPluginClassName(String name) {
  final String camelizedName = camelCase(name);
  return camelizedName[0].toUpperCase() + camelizedName.substring(1);
568 569
}

570
String _createUTIIdentifier(String organization, String name) {
571
  // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name
572
  final RegExp disallowed = RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+');
573 574
  name = camelCase(name).replaceAll(disallowed, '');
  name = name.isEmpty ? 'untitled' : name;
575
  return '$organization.$name';
576
}
577

578
final Set<String> _packageDependencies = Set<String>.from(<String>[
579
  'analyzer',
580 581 582 583
  'args',
  'async',
  'collection',
  'convert',
584
  'crypto',
585
  'flutter',
586 587
  'flutter_test',
  'front_end',
588
  'html',
589
  'http',
590
  'intl',
591 592 593
  'io',
  'isolate',
  'kernel',
594 595
  'logging',
  'matcher',
596
  'meta',
597 598 599 600 601 602 603 604 605 606
  'mime',
  'path',
  'plugin',
  'pool',
  'test',
  'utf',
  'watcher',
  'yaml'
]);

607
/// Return null if the project name is legal. Return a validation message if
608 609
/// we should disallow the project name.
String _validateProjectName(String projectName) {
610
  if (!linter_utils.isValidPackageName(projectName)) {
611
    final String packageNameDetails = package_names.PubPackageNames().details;
612 613
    return '"$projectName" is not a valid Dart package name.\n\n$packageNameDetails';
  }
614 615
  if (_packageDependencies.contains(projectName)) {
    return "Invalid project name: '$projectName' - this will conflict with Flutter "
616
      'package dependencies.';
617
  }
618 619
  return null;
}
620

621
/// Return null if the project directory is legal. Return a validation message
622
/// if we should disallow the directory name.
623
String _validateProjectDir(String dirPath, { String flutterRoot, bool overwrite = false }) {
624
  if (fs.path.isWithin(flutterRoot, dirPath)) {
625
    return 'Cannot create a project within the Flutter SDK. '
626 627 628
      "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
  }

629 630 631 632 633 634 635 636 637 638
  // 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;

639
  final FileSystemEntityType type = fs.typeSync(dirPath);
640

641
  if (type != FileSystemEntityType.notFound) {
642
    switch (type) {
643
      case FileSystemEntityType.file:
644
        // Do not overwrite files.
645
        return "Invalid project name: '$dirPath' - file exists.";
646
      case FileSystemEntityType.link:
647
        // Do not overwrite links.
648
        return "Invalid project name: '$dirPath' - refers to a link.";
649 650
    }
  }
651

652 653
  return null;
}