template.dart 7.57 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:mustache/mustache.dart' as mustache;
6

7
import 'base/common.dart';
8
import 'base/file_system.dart';
9
import 'cache.dart';
10 11 12 13 14 15 16 17 18 19 20
import 'globals.dart';

/// Expands templates in a directory to a destination. All files that must
/// undergo template expansion should end with the '.tmpl' extension. All other
/// files are ignored. In case the contents of entire directories must be copied
/// as is, the directory itself can end with '.tmpl' extension. Files within
/// such a directory may also contain the '.tmpl' extension and will be
/// considered for expansion. In case certain files need to be copied but
/// without template expansion (images, data files, etc.), the '.copy.tmpl'
/// extension may be used.
///
21 22 23 24 25
/// Folders with platform/language-specific content must be named
/// '<platform>-<language>.tmpl'.
///
/// Files in the destination will contain none of the '.tmpl', '.copy.tmpl'
/// or '-<language>.tmpl' extensions.
26 27
class Template {
  Template(Directory templateSource, Directory baseDir) {
28
    _templateFilePaths = <String, String>{};
29 30 31 32 33

    if (!templateSource.existsSync()) {
      return;
    }

34
    final List<FileSystemEntity> templateFiles = templateSource.listSync(recursive: true);
35 36 37 38 39 40 41

    for (FileSystemEntity entity in templateFiles) {
      if (entity is! File) {
        // We are only interesting in template *file* URIs.
        continue;
      }

42
      final String relativePath = fs.path.relative(entity.path,
43 44
          from: baseDir.absolute.path);

45
      if (relativePath.contains(templateExtension)) {
46 47 48
        // If '.tmpl' appears anywhere within the path of this entity, it is
        // is a candidate for rendering. This catches cases where the folder
        // itself is a template.
49
        _templateFilePaths[relativePath] = fs.path.absolute(entity.path);
50 51 52 53
      }
    }
  }

Ian Hickson's avatar
Ian Hickson committed
54 55
  factory Template.fromName(String name) {
    // All named templates are placed in the 'templates' directory
56
    final Directory templateDir = templateDirectoryInPackage(name);
57
    return Template(templateDir, templateDir);
Ian Hickson's avatar
Ian Hickson committed
58 59
  }

60 61
  static const String templateExtension = '.tmpl';
  static const String copyTemplateExtension = '.copy.tmpl';
62
  final Pattern _kTemplateLanguageVariant = RegExp(r'(\w+)-(\w+)\.tmpl.*');
63

64 65
  Map<String /* relative */, String /* absolute source */> _templateFilePaths;

66 67 68
  /// Render the template into [directory].
  ///
  /// May throw a [ToolExit] if the directory is not writable.
69 70 71
  int render(
    Directory destination,
    Map<String, dynamic> context, {
72
    bool overwriteExisting = true,
73
    bool printStatusWhenWriting = true,
74
  }) {
75 76 77 78 79 80 81
    try {
      destination.createSync(recursive: true);
    } on FileSystemException catch (err) {
      printError(err.toString());
      throwToolExit('Failed to flutter create at ${destination.path}.');
      return 0;
    }
Devon Carew's avatar
Devon Carew committed
82
    int fileCount = 0;
83

84 85 86 87 88 89 90 91 92 93
    /// Returns the resolved destination path corresponding to the specified
    /// raw destination path, after performing language filtering and template
    /// expansion on the path itself.
    ///
    /// Returns null if the given raw destination path has been filtered.
    String renderPath(String relativeDestinationPath) {
      final Match match = _kTemplateLanguageVariant.matchAsPrefix(relativeDestinationPath);
      if (match != null) {
        final String platform = match.group(1);
        final String language = context['${platform}Language'];
94
        if (language != match.group(2)) {
95
          return null;
96
        }
97 98
        relativeDestinationPath = relativeDestinationPath.replaceAll('$platform-$language.tmpl', platform);
      }
99 100 101 102 103
      // Only build a web project if explicitly asked.
      final bool web = context['web'];
      if (relativeDestinationPath.contains('web') && !web) {
        return null;
      }
104 105 106 107 108
      // Only build a macOS project if explicitly asked.
      final bool macOS = context['macos'];
      if (relativeDestinationPath.startsWith('macos.tmpl') && !macOS) {
        return null;
      }
109
      final String projectName = context['projectName'];
110
      final String androidIdentifier = context['androidIdentifier'];
111 112
      final String pluginClass = context['pluginClass'];
      final String destinationDirPath = destination.absolute.path;
113
      final String pathSeparator = fs.path.separator;
114
      String finalDestinationPath = fs.path
115
        .join(destinationDirPath, relativeDestinationPath)
116 117
        .replaceAll(copyTemplateExtension, '')
        .replaceAll(templateExtension, '');
118 119 120 121 122

      if (androidIdentifier != null) {
        finalDestinationPath = finalDestinationPath
            .replaceAll('androidIdentifier', androidIdentifier.replaceAll('.', pathSeparator));
      }
123
      if (projectName != null) {
124
        finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName);
125 126
      }
      if (pluginClass != null) {
127
        finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass);
128
      }
129 130 131 132
      return finalDestinationPath;
    }

    _templateFilePaths.forEach((String relativeDestinationPath, String absoluteSourcePath) {
133
      final bool withRootModule = context['withRootModule'] ?? false;
134
      if (!withRootModule && absoluteSourcePath.contains('flutter_root')) {
135
        return;
136
      }
137

138
      final String finalDestinationPath = renderPath(relativeDestinationPath);
139
      if (finalDestinationPath == null) {
140
        return;
141
      }
142 143
      final File finalDestinationFile = fs.file(finalDestinationPath);
      final String relativePathForLogging = fs.path.relative(finalDestinationFile.path);
144 145 146 147 148

      // Step 1: Check if the file needs to be overwritten.

      if (finalDestinationFile.existsSync()) {
        if (overwriteExisting) {
149
          finalDestinationFile.deleteSync(recursive: true);
150
          if (printStatusWhenWriting) {
151
            printStatus('  $relativePathForLogging (overwritten)');
152
          }
153 154
        } else {
          // The file exists but we cannot overwrite it, move on.
155
          if (printStatusWhenWriting) {
156
            printTrace('  $relativePathForLogging (existing - skipped)');
157
          }
158 159 160
          return;
        }
      } else {
161
        if (printStatusWhenWriting) {
162
          printStatus('  $relativePathForLogging (created)');
163
        }
164 165
      }

Devon Carew's avatar
Devon Carew committed
166 167
      fileCount++;

168
      finalDestinationFile.createSync(recursive: true);
169
      final File sourceFile = fs.file(absoluteSourcePath);
170

171
      // Step 2: If the absolute paths ends with a '.copy.tmpl', this file does
172 173
      //         not need mustache rendering but needs to be directly copied.

174 175
      if (sourceFile.path.endsWith(copyTemplateExtension)) {
        sourceFile.copySync(finalDestinationFile.path);
176 177 178 179 180 181 182

        return;
      }

      // Step 3: If the absolute path ends with a '.tmpl', this file needs
      //         rendering via mustache.

183
      if (sourceFile.path.endsWith(templateExtension)) {
184
        final String templateContents = sourceFile.readAsStringSync();
185
        final String renderedContents = mustache.Template(templateContents).renderString(context);
186 187 188 189 190 191 192 193 194

        finalDestinationFile.writeAsStringSync(renderedContents);

        return;
      }

      // Step 4: This file does not end in .tmpl but is in a directory that
      //         does. Directly copy the file to the destination.

195
      sourceFile.copySync(finalDestinationFile.path);
196
    });
Devon Carew's avatar
Devon Carew committed
197 198

    return fileCount;
199 200 201
  }
}

202
Directory templateDirectoryInPackage(String name) {
203
  final String templatesDir = fs.path.join(Cache.flutterRoot,
204
      'packages', 'flutter_tools', 'templates');
205
  return fs.directory(fs.path.join(templatesDir, name));
206
}