OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
| 5 import 'package:args/args.dart' show ArgParser, ArgResults; |
5 import 'package:path/path.dart' as path; | 6 import 'package:path/path.dart' as path; |
6 | 7 |
7 import '../js_ast/js_ast.dart'; | 8 import '../js_ast/js_ast.dart'; |
8 import 'js_names.dart'; | 9 import 'js_names.dart'; |
9 | 10 |
| 11 /// The module format to emit. |
| 12 enum ModuleFormat { |
| 13 /// ECMAScript 6 module using import and export. |
| 14 es6, |
| 15 |
| 16 /// CommonJS module (used in Node.js) |
| 17 common, |
| 18 |
| 19 /// Asynchronous Module Definition (AMD, used in browsers) |
| 20 amd, |
| 21 |
| 22 /// Dart Dev Compiler's legacy format (deprecated). |
| 23 legacy |
| 24 } |
| 25 |
| 26 /// Parses a string into a [ModuleFormat]. |
| 27 ModuleFormat parseModuleFormat(String s) => { |
| 28 'es6': ModuleFormat.es6, |
| 29 'common': ModuleFormat.common, |
| 30 'amd': ModuleFormat.amd, |
| 31 // Deprecated: |
| 32 'node': ModuleFormat.common, |
| 33 'legacy': ModuleFormat.legacy |
| 34 }[s]; |
| 35 |
| 36 /// Parse the module format option added by [addModuleFormatOptions]. |
| 37 List<ModuleFormat> parseModuleFormatOption(ArgResults argResults) { |
| 38 var format = argResults['modules']; |
| 39 if (format is String) { |
| 40 return [parseModuleFormat(format)]; |
| 41 } |
| 42 return (format as List<String>).map(parseModuleFormat).toList(); |
| 43 } |
| 44 |
| 45 /// Adds an option to the [argParser] for choosing the module format, optionally |
| 46 /// [allowMultiple] formats to be specified, with each emitted into a separate |
| 47 /// file. |
| 48 void addModuleFormatOptions(ArgParser argParser, {bool allowMultiple: false}) { |
| 49 argParser.addOption('modules', |
| 50 help: 'module pattern to emit', |
| 51 allowed: [ |
| 52 'es6', |
| 53 'common', |
| 54 'amd', |
| 55 'legacy', // deprecated |
| 56 'node', // renamed to commonjs |
| 57 'all' // to emit all flavors for the SDK |
| 58 ], |
| 59 allowedHelp: { |
| 60 'es6': 'ECMAScript 6 modules', |
| 61 'common': 'CommonJS/Node.js modules', |
| 62 'amd': 'AMD/RequireJS modules' |
| 63 }, |
| 64 allowMultiple: allowMultiple, |
| 65 defaultsTo: 'amd'); |
| 66 } |
| 67 |
| 68 /// Transforms an ES6 [module] into a given module [format]. |
| 69 /// |
| 70 /// If the format is [ModuleFormat.es6] this will return [module] unchanged. |
| 71 /// |
| 72 /// Because JS ASTs are immutable the resulting module will share as much |
| 73 /// structure as possible with the original. The transformation is a shallow one |
| 74 /// that affects the top-level module items, especially [ImportDeclaration]s and |
| 75 /// [ExportDeclaration]s. |
| 76 Program transformModuleFormat(ModuleFormat format, Program module) { |
| 77 switch (format) { |
| 78 case ModuleFormat.legacy: |
| 79 return new LegacyModuleBuilder().build(module); |
| 80 case ModuleFormat.common: |
| 81 return new CommonJSModuleBuilder().build(module); |
| 82 case ModuleFormat.amd: |
| 83 return new AmdModuleBuilder().build(module); |
| 84 case ModuleFormat.es6: |
| 85 return module; |
| 86 } |
| 87 return null; // unreachable. suppresses a bogus analyzer message |
| 88 } |
| 89 |
10 /// Base class for compiling ES6 modules into various ES5 module patterns. | 90 /// Base class for compiling ES6 modules into various ES5 module patterns. |
11 /// | 91 /// |
12 /// This is a helper class for utilities and state that is shared by several | 92 /// This is a helper class for utilities and state that is shared by several |
13 /// module transformers. | 93 /// module transformers. |
14 // TODO(jmesserly): "module transformer" might be a better name than builder. | 94 // TODO(jmesserly): "module transformer" might be a better name than builder. |
15 abstract class _ModuleBuilder { | 95 abstract class _ModuleBuilder { |
16 final imports = <ImportDeclaration>[]; | 96 final imports = <ImportDeclaration>[]; |
17 final exports = <ExportDeclaration>[]; | 97 final exports = <ExportDeclaration>[]; |
18 final statements = <Statement>[]; | 98 final statements = <Statement>[]; |
19 | 99 |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
54 // Collect imports/exports/statements. | 134 // Collect imports/exports/statements. |
55 visitProgram(module); | 135 visitProgram(module); |
56 | 136 |
57 // Build import parameters. | 137 // Build import parameters. |
58 var exportsVar = new TemporaryId('exports'); | 138 var exportsVar = new TemporaryId('exports'); |
59 var parameters = <TemporaryId>[exportsVar]; | 139 var parameters = <TemporaryId>[exportsVar]; |
60 var importNames = <Expression>[]; | 140 var importNames = <Expression>[]; |
61 var importStatements = <Statement>[]; | 141 var importStatements = <Statement>[]; |
62 for (var import in imports) { | 142 for (var import in imports) { |
63 importNames.add(import.from); | 143 importNames.add(import.from); |
64 // TODO(jmesserly): we could use destructuring once Atom supports it. | 144 // TODO(jmesserly): we could use destructuring here. |
65 var moduleVar = | 145 var moduleVar = |
66 new TemporaryId(pathToJSIdentifier(import.from.valueWithoutQuotes)); | 146 new TemporaryId(pathToJSIdentifier(import.from.valueWithoutQuotes)); |
67 parameters.add(moduleVar); | 147 parameters.add(moduleVar); |
68 for (var importName in import.namedImports) { | 148 for (var importName in import.namedImports) { |
69 assert(!importName.isStar); // import * not supported in legacy modules. | 149 assert(!importName.isStar); // import * not supported in legacy modules. |
70 var asName = importName.asName ?? importName.name; | 150 var asName = importName.asName ?? importName.name; |
71 importStatements.add(js.statement( | 151 importStatements.add(js.statement( |
72 'const # = #.#', [asName, moduleVar, importName.name.name])); | 152 'const # = #.#', [asName, moduleVar, importName.name.name])); |
73 } | 153 } |
74 } | 154 } |
(...skipping 23 matching lines...) Expand all Loading... |
98 js.string(module.name, "'"), | 178 js.string(module.name, "'"), |
99 new LiteralNull(), | 179 new LiteralNull(), |
100 js.commentExpression( | 180 js.commentExpression( |
101 "Imports", new ArrayInitializer(importNames, multiline: true)), | 181 "Imports", new ArrayInitializer(importNames, multiline: true)), |
102 resultModule | 182 resultModule |
103 ]); | 183 ]); |
104 return new Program(<ModuleItem>[moduleDef]); | 184 return new Program(<ModuleItem>[moduleDef]); |
105 } | 185 } |
106 } | 186 } |
107 | 187 |
108 /// Generates node modules. | 188 /// Generates CommonJS modules (used by Node.js). |
109 class NodeModuleBuilder extends _ModuleBuilder { | 189 class CommonJSModuleBuilder extends _ModuleBuilder { |
110 Program build(Program module) { | 190 Program build(Program module) { |
111 var importStatements = <Statement>[]; | 191 var importStatements = <Statement>[]; |
112 | 192 |
113 // Collect imports/exports/statements. | 193 // Collect imports/exports/statements. |
114 visitProgram(module); | 194 visitProgram(module); |
115 | 195 |
116 for (var import in imports) { | 196 for (var import in imports) { |
117 // TODO(jmesserly): we could use destructuring once Atom supports it. | 197 // TODO(jmesserly): we could use destructuring here. |
118 var moduleVar = | 198 var moduleVar = |
119 new TemporaryId(pathToJSIdentifier(import.from.valueWithoutQuotes)); | 199 new TemporaryId(pathToJSIdentifier(import.from.valueWithoutQuotes)); |
120 importStatements | 200 importStatements |
121 .add(js.statement('const # = require(#);', [moduleVar, import.from])); | 201 .add(js.statement('const # = require(#);', [moduleVar, import.from])); |
122 | 202 |
123 // TODO(jmesserly): optimize for the common case of a single import. | 203 // TODO(jmesserly): optimize for the common case of a single import. |
124 for (var importName in import.namedImports) { | 204 for (var importName in import.namedImports) { |
125 assert(!importName.isStar); // import * not supported yet. | 205 // import * is not emitted by the compiler, so we don't support it here. |
| 206 assert(!importName.isStar); |
126 var asName = importName.asName ?? importName.name; | 207 var asName = importName.asName ?? importName.name; |
127 importStatements.add(js.statement( | 208 importStatements.add(js.statement( |
128 'const # = #.#', [asName, moduleVar, importName.name.name])); | 209 'const # = #.#', [asName, moduleVar, importName.name.name])); |
129 } | 210 } |
130 } | 211 } |
131 statements.insertAll(0, importStatements); | 212 statements.insertAll(0, importStatements); |
132 | 213 |
133 if (exports.isNotEmpty) { | 214 if (exports.isNotEmpty) { |
134 var exportsVar = new Identifier('exports'); | 215 var exportsVar = new Identifier('exports'); |
135 statements.add(js.comment('Exports:')); | 216 statements.add(js.comment('Exports:')); |
136 for (var export in exports) { | 217 for (var export in exports) { |
137 var names = export.exportedNames; | 218 var names = export.exportedNames; |
138 assert(names != null); // export * not supported in legacy modules. | 219 // export * is not emitted by the compiler, so we don't handle it here. |
| 220 assert(names != null); |
139 for (var name in names) { | 221 for (var name in names) { |
140 statements | 222 statements |
141 .add(js.statement('#.# = #;', [exportsVar, name.name, name])); | 223 .add(js.statement('#.# = #;', [exportsVar, name.name, name])); |
142 } | 224 } |
143 } | 225 } |
144 } | 226 } |
145 | 227 |
146 // TODO(vsm): See https://github.com/dart-lang/dev_compiler/issues/512 | 228 // TODO(vsm): See https://github.com/dart-lang/dev_compiler/issues/512 |
147 // This extra level of indirection should be unnecessary. | 229 // This extra level of indirection should be unnecessary. |
148 var block = | 230 var block = |
149 js.statement("(function() { 'use strict'; #; })()", [statements]); | 231 js.statement("(function() { 'use strict'; #; })()", [statements]); |
150 | 232 |
151 return new Program([block]); | 233 return new Program([block]); |
152 } | 234 } |
153 } | 235 } |
154 | 236 |
| 237 /// Generates AMD modules (used in browsers with RequireJS). |
| 238 class AmdModuleBuilder extends _ModuleBuilder { |
| 239 Program build(Program module) { |
| 240 var importStatements = <Statement>[]; |
| 241 |
| 242 // Collect imports/exports/statements. |
| 243 visitProgram(module); |
| 244 |
| 245 var dependencies = <LiteralString>[]; |
| 246 var fnParams = <Parameter>[]; |
| 247 for (var import in imports) { |
| 248 // TODO(jmesserly): we could use destructuring once Atom supports it. |
| 249 var moduleVar = |
| 250 new TemporaryId(pathToJSIdentifier(import.from.valueWithoutQuotes)); |
| 251 fnParams.add(moduleVar); |
| 252 dependencies.add(import.from); |
| 253 |
| 254 // TODO(jmesserly): optimize for the common case of a single import. |
| 255 for (var importName in import.namedImports) { |
| 256 // import * is not emitted by the compiler, so we don't handle it here. |
| 257 assert(!importName.isStar); |
| 258 var asName = importName.asName ?? importName.name; |
| 259 importStatements.add(js.statement( |
| 260 'const # = #.#', [asName, moduleVar, importName.name.name])); |
| 261 } |
| 262 } |
| 263 statements.insertAll(0, importStatements); |
| 264 |
| 265 if (exports.isNotEmpty) { |
| 266 var exportedProps = <Property>[]; |
| 267 for (var export in exports) { |
| 268 var names = export.exportedNames; |
| 269 // export * is not emitted by the compiler, so we don't handle it here. |
| 270 assert(names != null); |
| 271 for (var name in names) { |
| 272 exportedProps.add(new Property(js.string(name.name), name)); |
| 273 } |
| 274 } |
| 275 statements.add(js.comment('Exports:')); |
| 276 statements.add( |
| 277 new Return(new ObjectInitializer(exportedProps, multiline: true))); |
| 278 } |
| 279 |
| 280 var block = js.statement("define(#, function(#) { 'use strict'; #; });", |
| 281 [new ArrayInitializer(dependencies), fnParams, statements]); |
| 282 |
| 283 return new Program([block]); |
| 284 } |
| 285 } |
| 286 |
155 /// Escape [name] to make it into a valid identifier. | 287 /// Escape [name] to make it into a valid identifier. |
156 String pathToJSIdentifier(String name) { | 288 String pathToJSIdentifier(String name) { |
157 return toJSIdentifier(path.basenameWithoutExtension(name)); | 289 return toJSIdentifier(path.basenameWithoutExtension(name)); |
158 } | 290 } |
159 | 291 |
160 /// Escape [name] to make it into a valid identifier. | 292 /// Escape [name] to make it into a valid identifier. |
161 String toJSIdentifier(String name) { | 293 String toJSIdentifier(String name) { |
162 if (name.length == 0) return r'$'; | 294 if (name.length == 0) return r'$'; |
163 | 295 |
164 // Escape any invalid characters | 296 // Escape any invalid characters |
(...skipping 13 matching lines...) Expand all Loading... |
178 // Ensure the identifier first character is not numeric and that the whole | 310 // Ensure the identifier first character is not numeric and that the whole |
179 // identifier is not a keyword. | 311 // identifier is not a keyword. |
180 if (result.startsWith(new RegExp('[0-9]')) || invalidVariableName(result)) { | 312 if (result.startsWith(new RegExp('[0-9]')) || invalidVariableName(result)) { |
181 return '\$$result'; | 313 return '\$$result'; |
182 } | 314 } |
183 return result; | 315 return result; |
184 } | 316 } |
185 | 317 |
186 // Invalid characters for identifiers, which would need to be escaped. | 318 // Invalid characters for identifiers, which would need to be escaped. |
187 final _invalidCharInIdentifier = new RegExp(r'[^A-Za-z_$0-9]'); | 319 final _invalidCharInIdentifier = new RegExp(r'[^A-Za-z_$0-9]'); |
OLD | NEW |