cc7fcb5f70
Change-Id: I0111e74d312f6d3aeebb80f017a4f48fc6186109 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/218220 Reviewed-by: Nicholas Shahan <nshahan@google.com> Commit-Queue: Kevin Moore <kevmoo@google.com>
515 lines
17 KiB
Dart
515 lines
17 KiB
Dart
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
// @dart = 2.9
|
|
|
|
import 'package:args/args.dart' show ArgParser, ArgResults;
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import '../js_ast/js_ast.dart';
|
|
import 'js_names.dart';
|
|
import 'shared_compiler.dart';
|
|
|
|
/// The module format to emit.
|
|
enum ModuleFormat {
|
|
/// ECMAScript 6 module using import and export.
|
|
es6,
|
|
|
|
/// CommonJS module (used in Node.js)
|
|
common,
|
|
|
|
/// Asynchronous Module Definition (AMD, used in browsers).
|
|
amd,
|
|
|
|
/// Dart Dev Compiler's own format.
|
|
ddc,
|
|
}
|
|
|
|
/// Parses a string into a [ModuleFormat].
|
|
///
|
|
/// Throws an [ArgumentError] if the module format is not recognized.
|
|
ModuleFormat parseModuleFormat(String s) {
|
|
var formats = const {
|
|
'es6': ModuleFormat.es6,
|
|
'common': ModuleFormat.common,
|
|
'amd': ModuleFormat.amd,
|
|
'ddc': ModuleFormat.ddc,
|
|
// Deprecated:
|
|
'node': ModuleFormat.common,
|
|
'legacy': ModuleFormat.ddc
|
|
};
|
|
var selected = formats[s];
|
|
if (selected == null) {
|
|
throw ArgumentError('Invalid module format `$s`, allowed formats are: '
|
|
'`${formats.keys.join(', ')}`');
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
/// Parse the module format option added by [addModuleFormatOptions].
|
|
List<ModuleFormat> parseModuleFormatOption(ArgResults args) {
|
|
return (args['modules'] as List<String>).map(parseModuleFormat).toList();
|
|
}
|
|
|
|
/// Adds an option to the [argParser] for choosing the module format, optionally
|
|
/// [allowMultiple] formats to be specified, with each emitted into a separate
|
|
/// file.
|
|
void addModuleFormatOptions(ArgParser argParser, {bool hide = true}) {
|
|
argParser.addMultiOption('modules', help: 'module pattern to emit', allowed: [
|
|
'es6',
|
|
'common',
|
|
'amd',
|
|
'ddc',
|
|
'legacy', // renamed to ddc
|
|
'node', // renamed to commonjs
|
|
'all' // to emit all flavors for the SDK
|
|
], allowedHelp: {
|
|
'es6': 'ECMAScript 6 modules',
|
|
'common': 'CommonJS/Node.js modules',
|
|
'amd': 'AMD/RequireJS modules'
|
|
}, defaultsTo: [
|
|
'amd'
|
|
]);
|
|
}
|
|
|
|
/// Transforms an ES6 [module] into a given module [format].
|
|
///
|
|
/// If the format is [ModuleFormat.es6] this will return [module] unchanged.
|
|
///
|
|
/// Because JS ASTs are immutable the resulting module will share as much
|
|
/// structure as possible with the original. The transformation is a shallow one
|
|
/// that affects the top-level module items, especially [ImportDeclaration]s and
|
|
/// [ExportDeclaration]s.
|
|
Program transformModuleFormat(ModuleFormat format, Program module) {
|
|
switch (format) {
|
|
case ModuleFormat.ddc:
|
|
// Legacy format always generates output compatible with single file mode.
|
|
return DdcModuleBuilder().build(module);
|
|
case ModuleFormat.common:
|
|
return CommonJSModuleBuilder().build(module);
|
|
case ModuleFormat.amd:
|
|
return AmdModuleBuilder().build(module);
|
|
case ModuleFormat.es6:
|
|
default:
|
|
return module;
|
|
}
|
|
}
|
|
|
|
/// Transforms an ES6 [function] into a given module [format].
|
|
///
|
|
/// If the format is [ModuleFormat.es6] this will return [function] unchanged.
|
|
///
|
|
/// Because JS ASTs are immutable the resulting function will share as much
|
|
/// structure as possible with the original. The transformation is a shallow one
|
|
/// that affects the [ImportDeclaration]s from [items].
|
|
///
|
|
/// Returns a new function that combines all statements from tranformed imports
|
|
/// from [items] and the body of the [function].
|
|
Fun transformFunctionModuleFormat(
|
|
List<ModuleItem> items, Fun function, ModuleFormat format) {
|
|
switch (format) {
|
|
case ModuleFormat.ddc:
|
|
// Legacy format always generates output compatible with single file mode.
|
|
return DdcModuleBuilder().buildFunctionWithImports(items, function);
|
|
case ModuleFormat.amd:
|
|
return AmdModuleBuilder().buildFunctionWithImports(items, function);
|
|
default:
|
|
throw UnsupportedError(
|
|
'Incremental build does not support $format module format');
|
|
}
|
|
}
|
|
|
|
/// Base class for compiling ES6 modules into various ES5 module patterns.
|
|
///
|
|
/// This is a helper class for utilities and state that is shared by several
|
|
/// module transformers.
|
|
// TODO(jmesserly): "module transformer" might be a better name than builder.
|
|
abstract class _ModuleBuilder {
|
|
final imports = <ImportDeclaration>[];
|
|
final exports = <ExportDeclaration>[];
|
|
final statements = <Statement>[];
|
|
|
|
/// Collect [imports], [exports] and [statements] from the ES6 [module].
|
|
void visitProgram(Program module) {
|
|
visitModuleItems(module.body);
|
|
}
|
|
|
|
/// Collect [imports], [exports] and [statements] from the ES6 [items].
|
|
///
|
|
/// For exports, this will also add their body to [statements] in the
|
|
/// appropriate position.
|
|
void visitModuleItems(List<ModuleItem> items) {
|
|
for (var item in items) {
|
|
if (item is ImportDeclaration) {
|
|
visitImportDeclaration(item);
|
|
} else if (item is ExportDeclaration) {
|
|
visitExportDeclaration(item);
|
|
} else if (item is Statement) {
|
|
visitStatement(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
void visitImportDeclaration(ImportDeclaration node) {
|
|
imports.add(node);
|
|
}
|
|
|
|
void visitExportDeclaration(ExportDeclaration node) {
|
|
exports.add(node);
|
|
var exported = node.exported;
|
|
if (exported is! ExportClause) {
|
|
statements.add(exported.toStatement());
|
|
}
|
|
}
|
|
|
|
void visitStatement(Statement node) {
|
|
statements.add(node);
|
|
}
|
|
|
|
void clear() {
|
|
imports.clear();
|
|
exports.clear();
|
|
statements.clear();
|
|
}
|
|
}
|
|
|
|
/// Generates modules for with our DDC `dart_library.js` loading mechanism.
|
|
// TODO(jmesserly): remove this and replace with something that interoperates.
|
|
class DdcModuleBuilder extends _ModuleBuilder {
|
|
/// Build a module variable definition for [import].
|
|
///
|
|
/// Used to load modules referenced in the expression during expression
|
|
/// evaluation.
|
|
static Statement buildLoadModule(
|
|
Identifier moduleVar, ImportDeclaration import) =>
|
|
js.statement(
|
|
'const # = dart_library.import(#);', [moduleVar, import.from]);
|
|
|
|
/// Build library variable definitions for all libraries from [import].
|
|
static List<Statement> buildImports(
|
|
Identifier moduleVar, ImportDeclaration import, bool deferModules) {
|
|
var items = <Statement>[];
|
|
|
|
for (var importName in import.namedImports) {
|
|
// import * is not emitted by the compiler, so we don't handle it here.
|
|
assert(!importName.isStar);
|
|
var asName = importName.asName ?? importName.name;
|
|
var fromName = importName.name.name;
|
|
// Load non-SDK modules on demand (i.e., deferred).
|
|
if (deferModules && import.from.valueWithoutQuotes != dartSdkModule) {
|
|
items.add(js.statement(
|
|
'let # = dart_library.defer(#, #, function (mod, lib) {'
|
|
' # = mod;'
|
|
' # = lib;'
|
|
'});',
|
|
[asName, moduleVar, js.string(fromName), moduleVar, asName]));
|
|
} else {
|
|
items.add(js.statement('const # = #.#', [asName, moduleVar, fromName]));
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/// Build statements for [exports].
|
|
static List<Statement> buildExports(
|
|
Identifier exportsVar, List<ExportDeclaration> exports) {
|
|
var items = <Statement>[];
|
|
|
|
if (exports.isNotEmpty) {
|
|
items.add(js.comment('Exports:'));
|
|
// TODO(jmesserly): make these immutable in JS?
|
|
for (var export in exports) {
|
|
var names = export.exportedNames;
|
|
assert(names != null); // export * not supported in ddc modules.
|
|
for (var name in names) {
|
|
var alias = name.asName ?? name.name;
|
|
items.add(
|
|
js.statement('#.# = #;', [exportsVar, alias.name, name.name]));
|
|
}
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/// Build function body with all necessary imports included.
|
|
///
|
|
/// Used for the top level synthetic function generated during expression
|
|
/// compilation, in order to include all the context needed for evaluation
|
|
/// inside it.
|
|
///
|
|
/// Returns a new function that combines all statements from tranformed
|
|
/// imports from [items] and the body of the [function].
|
|
Fun buildFunctionWithImports(List<ModuleItem> items, Fun function) {
|
|
clear();
|
|
visitModuleItems(items);
|
|
|
|
var moduleImports = _collectModuleImports(imports);
|
|
var importStatements = <Statement>[];
|
|
|
|
for (var p in moduleImports) {
|
|
var moduleVar = p.key;
|
|
var import = p.value;
|
|
importStatements.add(buildLoadModule(moduleVar, import));
|
|
importStatements.addAll(buildImports(moduleVar, import, false));
|
|
}
|
|
|
|
return Fun(
|
|
function.params,
|
|
Block([...importStatements, ...statements, ...function.body.statements]),
|
|
);
|
|
}
|
|
|
|
Program build(Program module) {
|
|
// Collect imports/exports/statements.
|
|
visitProgram(module);
|
|
|
|
var exportsVar = TemporaryId('exports');
|
|
var parameters = <Identifier>[exportsVar];
|
|
var importNames = <Expression>[];
|
|
|
|
var moduleImports = _collectModuleImports(imports);
|
|
var importStatements = <Statement>[];
|
|
|
|
for (var p in moduleImports) {
|
|
var moduleVar = p.key;
|
|
var import = p.value;
|
|
importNames.add(import.from);
|
|
parameters.add(moduleVar);
|
|
importStatements.addAll(buildImports(moduleVar, import, true));
|
|
}
|
|
|
|
// Prepend import statetements.
|
|
statements.insertAll(0, importStatements);
|
|
|
|
// Append export statements.
|
|
statements.addAll(buildExports(exportsVar, exports));
|
|
|
|
var resultModule = NamedFunction(
|
|
loadFunctionIdentifier(module.name),
|
|
js.fun("function(#) { 'use strict'; #; }", [parameters, statements]),
|
|
true);
|
|
|
|
var moduleDef = js.statement('dart_library.library(#, #, #, #, #)', [
|
|
js.string(module.name, "'"),
|
|
LiteralNull(),
|
|
js.commentExpression(
|
|
'Imports', ArrayInitializer(importNames, multiline: true)),
|
|
resultModule,
|
|
SharedCompiler.metricsLocationID
|
|
]);
|
|
return Program(<ModuleItem>[moduleDef]);
|
|
}
|
|
}
|
|
|
|
/// Generates CommonJS modules (used by Node.js).
|
|
class CommonJSModuleBuilder extends _ModuleBuilder {
|
|
Program build(Program module) {
|
|
var importStatements = [
|
|
js.statement("'use strict';"),
|
|
];
|
|
|
|
// Collect imports/exports/statements.
|
|
visitProgram(module);
|
|
|
|
for (var import in imports) {
|
|
// TODO(jmesserly): we could use destructuring here.
|
|
var moduleVar =
|
|
TemporaryId(pathToJSIdentifier(import.from.valueWithoutQuotes));
|
|
importStatements
|
|
.add(js.statement('const # = require(#);', [moduleVar, import.from]));
|
|
|
|
// TODO(jmesserly): optimize for the common case of a single import.
|
|
for (var importName in import.namedImports) {
|
|
// import * is not emitted by the compiler, so we don't support it here.
|
|
assert(!importName.isStar);
|
|
var asName = importName.asName ?? importName.name;
|
|
importStatements.add(js.statement(
|
|
'const # = #.#', [asName, moduleVar, importName.name.name]));
|
|
}
|
|
}
|
|
statements.insertAll(0, importStatements);
|
|
|
|
if (exports.isNotEmpty) {
|
|
var exportsVar = Identifier('exports');
|
|
statements.add(js.comment('Exports:'));
|
|
for (var export in exports) {
|
|
var names = export.exportedNames;
|
|
// export * is not emitted by the compiler, so we don't handle it here.
|
|
assert(names != null);
|
|
for (var name in names) {
|
|
var alias = name.asName ?? name.name;
|
|
statements.add(
|
|
js.statement('#.# = #;', [exportsVar, alias.name, name.name]));
|
|
}
|
|
}
|
|
}
|
|
|
|
return Program(statements);
|
|
}
|
|
}
|
|
|
|
/// Generates AMD modules (used in browsers with RequireJS).
|
|
class AmdModuleBuilder extends _ModuleBuilder {
|
|
AmdModuleBuilder();
|
|
|
|
/// Build a module variable definition for [import].
|
|
///
|
|
/// Used to load modules referenced in the expression during expression
|
|
/// evaluation.
|
|
static Statement buildLoadModule(
|
|
Identifier moduleVar, ImportDeclaration import) =>
|
|
js.statement('const # = require(#);', [moduleVar, import.from]);
|
|
|
|
/// Build library variable definitions for all libraries from [import].
|
|
static List<Statement> buildImports(
|
|
Identifier moduleVar, ImportDeclaration import) {
|
|
var items = <Statement>[];
|
|
|
|
for (var importName in import.namedImports) {
|
|
// import * is not emitted by the compiler, so we don't handle it here.
|
|
assert(!importName.isStar);
|
|
var asName = importName.asName ?? importName.name;
|
|
items.add(js.statement(
|
|
'const # = #.#', [asName, moduleVar, importName.name.name]));
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/// Build statements for [exports].
|
|
static List<Statement> buildExports(List<ExportDeclaration> exports) {
|
|
var items = <Statement>[];
|
|
|
|
if (exports.isNotEmpty) {
|
|
var exportedProps = <Property>[];
|
|
for (var export in exports) {
|
|
var names = export.exportedNames;
|
|
// export * is not emitted by the compiler, so we don't handle it here.
|
|
assert(names != null);
|
|
for (var name in names) {
|
|
var alias = name.asName ?? name.name;
|
|
exportedProps.add(Property(js.string(alias.name), name.name));
|
|
}
|
|
}
|
|
items.add(js.comment('Exports:'));
|
|
items.add(Return(ObjectInitializer(exportedProps, multiline: true)));
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/// Build function body with all necessary imports included.
|
|
///
|
|
/// Used for the top level synthetic function generated during expression
|
|
/// compilation, in order to include all the context needed for evaluation
|
|
/// inside it.
|
|
///
|
|
/// Returns a new function that combines all statements from tranformed
|
|
/// imports from [items] and the body of the [function].
|
|
Fun buildFunctionWithImports(List<ModuleItem> items, Fun function) {
|
|
clear();
|
|
visitModuleItems(items);
|
|
|
|
var moduleImports = _collectModuleImports(imports);
|
|
var importStatements = <Statement>[];
|
|
|
|
for (var p in moduleImports) {
|
|
var moduleVar = p.key;
|
|
var import = p.value;
|
|
importStatements.add(buildLoadModule(moduleVar, import));
|
|
importStatements.addAll(buildImports(moduleVar, import));
|
|
}
|
|
|
|
return Fun(
|
|
function.params,
|
|
Block([...importStatements, ...statements, ...function.body.statements]),
|
|
);
|
|
}
|
|
|
|
Program build(Program module) {
|
|
// Collect imports/exports/statements.
|
|
visitProgram(module);
|
|
|
|
var moduleImports = _collectModuleImports(imports);
|
|
var importStatements = <Statement>[];
|
|
var fnParams = <Identifier>[];
|
|
var dependencies = <LiteralString>[];
|
|
|
|
for (var p in moduleImports) {
|
|
var moduleVar = p.key;
|
|
var import = p.value;
|
|
fnParams.add(moduleVar);
|
|
dependencies.add(import.from);
|
|
importStatements.addAll(buildImports(moduleVar, import));
|
|
}
|
|
|
|
// Prepend import statetements.
|
|
statements.insertAll(0, importStatements);
|
|
|
|
// Append export statements.
|
|
statements.addAll(buildExports(exports));
|
|
|
|
var resultModule = NamedFunction(
|
|
loadFunctionIdentifier(module.name),
|
|
js.fun("function(#) { 'use strict'; #; }", [fnParams, statements]),
|
|
true);
|
|
var block = js.statement(
|
|
'define(#, #);', [ArrayInitializer(dependencies), resultModule]);
|
|
|
|
return Program([block]);
|
|
}
|
|
}
|
|
|
|
bool isSdkInternalRuntimeUri(Uri importUri) {
|
|
return importUri.scheme == 'dart' && importUri.path == '_runtime';
|
|
}
|
|
|
|
String libraryUriToJsIdentifier(Uri importUri) {
|
|
if (importUri.scheme == 'dart') {
|
|
return isSdkInternalRuntimeUri(importUri) ? 'dart' : importUri.path;
|
|
}
|
|
return pathToJSIdentifier(p.withoutExtension(importUri.pathSegments.last));
|
|
}
|
|
|
|
/// Converts an entire arbitrary path string into a string compatible with
|
|
/// JS identifier naming rules while conserving path information.
|
|
///
|
|
/// NOT guaranteed to result in a unique string. E.g.,
|
|
/// 1) '__' appears in a file name.
|
|
/// 2) An escaped '/' or '\' appears in a filename (a/b and a$47b).
|
|
String pathToJSIdentifier(String path) {
|
|
path = p.normalize(path);
|
|
if (path.startsWith('/') || path.startsWith('\\')) {
|
|
path = path.substring(1, path.length);
|
|
}
|
|
return toJSIdentifier(path
|
|
.replaceAll('\\', '__')
|
|
.replaceAll('/', '__')
|
|
.replaceAll('..', '__')
|
|
.replaceAll('-', '_'));
|
|
}
|
|
|
|
/// Creates function name given [moduleName].
|
|
String loadFunctionName(String moduleName) =>
|
|
'load__' + pathToJSIdentifier(moduleName.replaceAll('.', '_'));
|
|
|
|
/// Creates function name identifier given [moduleName].
|
|
Identifier loadFunctionIdentifier(String moduleName) =>
|
|
Identifier(loadFunctionName(moduleName));
|
|
|
|
// Replacement string for path separators (i.e., '/', '\', '..').
|
|
final encodedSeparator = '__';
|
|
|
|
/// Group libraries from [imports] by modules.
|
|
List<MapEntry<Identifier, ImportDeclaration>> _collectModuleImports(
|
|
List<ImportDeclaration> imports) {
|
|
var result = <MapEntry<Identifier, ImportDeclaration>>[];
|
|
for (var import in imports) {
|
|
// TODO(jmesserly): we could use destructuring once Atom supports it.
|
|
var moduleVar =
|
|
TemporaryId(pathToJSIdentifier(import.from.valueWithoutQuotes));
|
|
|
|
result.add(MapEntry<Identifier, ImportDeclaration>(moduleVar, import));
|
|
}
|
|
return result;
|
|
}
|