• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:mustache/mustache.dart' as mustache;
6
7import 'base/common.dart';
8import 'base/file_system.dart';
9import 'cache.dart';
10import 'globals.dart';
11
12/// Expands templates in a directory to a destination. All files that must
13/// undergo template expansion should end with the '.tmpl' extension. All other
14/// files are ignored. In case the contents of entire directories must be copied
15/// as is, the directory itself can end with '.tmpl' extension. Files within
16/// such a directory may also contain the '.tmpl' extension and will be
17/// considered for expansion. In case certain files need to be copied but
18/// without template expansion (images, data files, etc.), the '.copy.tmpl'
19/// extension may be used.
20///
21/// Folders with platform/language-specific content must be named
22/// '<platform>-<language>.tmpl'.
23///
24/// Files in the destination will contain none of the '.tmpl', '.copy.tmpl'
25/// or '-<language>.tmpl' extensions.
26class Template {
27  Template(Directory templateSource, Directory baseDir) {
28    _templateFilePaths = <String, String>{};
29
30    if (!templateSource.existsSync()) {
31      return;
32    }
33
34    final List<FileSystemEntity> templateFiles = templateSource.listSync(recursive: true);
35
36    for (FileSystemEntity entity in templateFiles) {
37      if (entity is! File) {
38        // We are only interesting in template *file* URIs.
39        continue;
40      }
41
42      final String relativePath = fs.path.relative(entity.path,
43          from: baseDir.absolute.path);
44
45      if (relativePath.contains(templateExtension)) {
46        // If '.tmpl' appears anywhere within the path of this entity, it is
47        // is a candidate for rendering. This catches cases where the folder
48        // itself is a template.
49        _templateFilePaths[relativePath] = fs.path.absolute(entity.path);
50      }
51    }
52  }
53
54  factory Template.fromName(String name) {
55    // All named templates are placed in the 'templates' directory
56    final Directory templateDir = templateDirectoryInPackage(name);
57    return Template(templateDir, templateDir);
58  }
59
60  static const String templateExtension = '.tmpl';
61  static const String copyTemplateExtension = '.copy.tmpl';
62  final Pattern _kTemplateLanguageVariant = RegExp(r'(\w+)-(\w+)\.tmpl.*');
63
64  Map<String /* relative */, String /* absolute source */> _templateFilePaths;
65
66  /// Render the template into [directory].
67  ///
68  /// May throw a [ToolExit] if the directory is not writable.
69  int render(
70    Directory destination,
71    Map<String, dynamic> context, {
72    bool overwriteExisting = true,
73    bool printStatusWhenWriting = true,
74  }) {
75    try {
76      destination.createSync(recursive: true);
77    } on FileSystemException catch (err) {
78      printError(err.toString());
79      throwToolExit('Failed to flutter create at ${destination.path}.');
80      return 0;
81    }
82    int fileCount = 0;
83
84    /// Returns the resolved destination path corresponding to the specified
85    /// raw destination path, after performing language filtering and template
86    /// expansion on the path itself.
87    ///
88    /// Returns null if the given raw destination path has been filtered.
89    String renderPath(String relativeDestinationPath) {
90      final Match match = _kTemplateLanguageVariant.matchAsPrefix(relativeDestinationPath);
91      if (match != null) {
92        final String platform = match.group(1);
93        final String language = context['${platform}Language'];
94        if (language != match.group(2))
95          return null;
96        relativeDestinationPath = relativeDestinationPath.replaceAll('$platform-$language.tmpl', platform);
97      }
98      // Only build a web project if explicitly asked.
99      final bool web = context['web'];
100      if (relativeDestinationPath.contains('web') && !web) {
101        return null;
102      }
103      final String projectName = context['projectName'];
104      final String androidIdentifier = context['androidIdentifier'];
105      final String pluginClass = context['pluginClass'];
106      final String destinationDirPath = destination.absolute.path;
107      final String pathSeparator = fs.path.separator;
108      String finalDestinationPath = fs.path
109        .join(destinationDirPath, relativeDestinationPath)
110        .replaceAll(copyTemplateExtension, '')
111        .replaceAll(templateExtension, '');
112
113      if (androidIdentifier != null) {
114        finalDestinationPath = finalDestinationPath
115            .replaceAll('androidIdentifier', androidIdentifier.replaceAll('.', pathSeparator));
116      }
117      if (projectName != null)
118        finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName);
119      if (pluginClass != null)
120        finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass);
121      return finalDestinationPath;
122    }
123
124    _templateFilePaths.forEach((String relativeDestinationPath, String absoluteSourcePath) {
125      final bool withRootModule = context['withRootModule'] ?? false;
126      if (!withRootModule && absoluteSourcePath.contains('flutter_root'))
127        return;
128
129      final String finalDestinationPath = renderPath(relativeDestinationPath);
130      if (finalDestinationPath == null)
131        return;
132      final File finalDestinationFile = fs.file(finalDestinationPath);
133      final String relativePathForLogging = fs.path.relative(finalDestinationFile.path);
134
135      // Step 1: Check if the file needs to be overwritten.
136
137      if (finalDestinationFile.existsSync()) {
138        if (overwriteExisting) {
139          finalDestinationFile.deleteSync(recursive: true);
140          if (printStatusWhenWriting)
141            printStatus('  $relativePathForLogging (overwritten)');
142        } else {
143          // The file exists but we cannot overwrite it, move on.
144          if (printStatusWhenWriting)
145            printTrace('  $relativePathForLogging (existing - skipped)');
146          return;
147        }
148      } else {
149        if (printStatusWhenWriting)
150          printStatus('  $relativePathForLogging (created)');
151      }
152
153      fileCount++;
154
155      finalDestinationFile.createSync(recursive: true);
156      final File sourceFile = fs.file(absoluteSourcePath);
157
158      // Step 2: If the absolute paths ends with a '.copy.tmpl', this file does
159      //         not need mustache rendering but needs to be directly copied.
160
161      if (sourceFile.path.endsWith(copyTemplateExtension)) {
162        sourceFile.copySync(finalDestinationFile.path);
163
164        return;
165      }
166
167      // Step 3: If the absolute path ends with a '.tmpl', this file needs
168      //         rendering via mustache.
169
170      if (sourceFile.path.endsWith(templateExtension)) {
171        final String templateContents = sourceFile.readAsStringSync();
172        final String renderedContents = mustache.Template(templateContents).renderString(context);
173
174        finalDestinationFile.writeAsStringSync(renderedContents);
175
176        return;
177      }
178
179      // Step 4: This file does not end in .tmpl but is in a directory that
180      //         does. Directly copy the file to the destination.
181
182      sourceFile.copySync(finalDestinationFile.path);
183    });
184
185    return fileCount;
186  }
187}
188
189Directory templateDirectoryInPackage(String name) {
190  final String templatesDir = fs.path.join(Cache.flutterRoot,
191      'packages', 'flutter_tools', 'templates');
192  return fs.directory(fs.path.join(templatesDir, name));
193}
194