blob: 78326c89ab7237b0923566339405ffe906f0a001 [file] [log] [blame]
// Copyright (c) 2023, 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.
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:args/args.dart';
import 'package:io/ansi.dart' as ansi;
import 'package:io/io.dart';
import 'package:path/path.dart' as p;
void main(List<String> arguments) async {
final ArgResults argResult;
try {
argResult = _parser.parse(arguments);
} on FormatException catch (e) {
print('''
${ansi.lightRed.wrap(e.message)}
$_usage''');
exitCode = ExitCode.usage.code;
return;
}
if (argResult['help'] as bool) {
print(_usage);
return;
}
assert(p.fromUri(Platform.script).endsWith(_thisScript));
// Run `npm install` or `npm update` as needed.
final update = argResult['update'] as bool;
await _runProc(
'npm',
[update ? 'update' : 'install'],
workingDirectory: _bindingsGeneratorPath,
);
// Compute JS type supertypes for union calculation in translator.
await _generateJsTypeSupertypes();
if (argResult['compile'] as bool) {
// Compile Dart to Javascript.
await _runProc(
Platform.executable,
[
'compile',
'js',
'--enable-asserts',
'dart_main.dart',
'-o',
'dart_main.js',
],
workingDirectory: _bindingsGeneratorPath,
);
}
// Determine the set of previously generated files.
final domDir = Directory(p.join('lib', 'src', 'dom'));
final existingFiles =
domDir.listSync(recursive: true).whereType<File>().where((file) {
if (!file.path.endsWith('.dart')) return false;
final contents = file.readAsStringSync();
return contents.contains('Generated from Web IDL definitions');
}).toList();
final timeStamps = {
for (final file in existingFiles) file.path: file.lastModifiedSync(),
};
// Run app with `node`.
await _runProc(
'node',
['main.mjs', '../../lib/src'],
workingDirectory: _bindingsGeneratorPath,
);
// Delete previously generated files that have not been updated.
for (final file in existingFiles) {
final stamp = timeStamps[file.path];
if (stamp == file.lastModifiedSync()) {
file.deleteSync();
}
}
// Update readme.
final readmeFile = File(
p.normalize(p.join(_bindingsGeneratorPath, '..', '..', 'README.md')),
);
final sourceContent = readmeFile.readAsStringSync();
final idlVersion = _packageLockVersion(_webRefIdl);
final cssVersion = _packageLockVersion(_webRefCss);
final versions = '''
$_startComment
- $_webRefIdl [$idlVersion](https://www.npmjs.com/package/$_webRefIdl/v/$idlVersion)
- $_webRefCss [$cssVersion](https://www.npmjs.com/package/$_webRefCss/v/$cssVersion)
''';
final newContent =
sourceContent.substring(0, sourceContent.indexOf(_startComment)) +
versions +
sourceContent.substring(sourceContent.indexOf(_endComment));
if (newContent == sourceContent) {
print(ansi.styleBold.wrap('No update for readme.'));
} else {
print(ansi.styleBold.wrap('Updating readme for IDL version $idlVersion'));
readmeFile.writeAsStringSync(newContent, mode: FileMode.writeOnly);
}
}
String _packageLockVersion(String package) {
final packageLockData = jsonDecode(
File(p.join(_bindingsGeneratorPath, 'package-lock.json'))
.readAsStringSync(),
) as Map<String, dynamic>;
final packages = packageLockData['packages'] as Map<String, dynamic>;
final webRefIdl = packages['node_modules/$package'] as Map<String, dynamic>;
return webRefIdl['version'] as String;
}
final _bindingsGeneratorPath = p.join('tool', 'generator');
const _webRefIdl = '@webref/idl';
const _webRefCss = '@webref/css';
const _thisScript = 'tool/update_bindings.dart';
const _startComment =
'<!-- START updated by $_thisScript. Do not modify by hand -->';
const _endComment =
'<!-- END updated by $_thisScript. Do not modify by hand -->';
Future<void> _runProc(
String executable,
List<String> arguments, {
required String workingDirectory,
}) async {
print(ansi.styleBold.wrap(['*', executable, ...arguments].join(' ')));
final proc = await Process.start(
executable,
arguments,
mode: ProcessStartMode.inheritStdio,
workingDirectory: workingDirectory,
);
final procExit = await proc.exitCode;
if (procExit != 0) {
throw ProcessException(executable, arguments, 'Process failed', procExit);
}
}
bool _isInJsTypesOrJsInterop(InterfaceElement element) =>
element.library.isInSdk &&
(element.library.name == '_js_types' ||
element is ExtensionTypeElement &&
element.library.name == 'dart.js_interop');
Future<void> _generateJsTypeSupertypes() async {
// Use a file that uses `dart:js_interop` for analysis.
final contextCollection = AnalysisContextCollection(includedPaths: [
p.fromUri(Platform.script.resolve('../lib/src/dom.dart'))
]);
final dartJsInterop = await contextCollection.contexts.single.currentSession
.getLibraryByUri('dart:js_interop') as LibraryElementResult;
final definedNames = dartJsInterop.element.exportNamespace.definedNames;
// `SplayTreeMap` to avoid moving types around in `dart:js_interop` affecting
// the code generation.
final jsTypeSupertypes = SplayTreeMap<String, String?>();
for (final name in definedNames.keys) {
final element = definedNames[name];
if (element is TypeDefiningElement) {
// TODO(srujzs): This contains code that handles the SDK before and after
// the migration of JS types to extension types. Once the changes to
// migrate to extension types hit the dev branch, we should remove some of
// the old code.
void storeSupertypes(InterfaceElement element) {
if (!_isInJsTypesOrJsInterop(element)) return;
String? parentJsType;
final supertype = element.supertype;
final immediateSupertypes = <InterfaceType>[
if (supertype != null) supertype,
...element.interfaces,
]..removeWhere((supertype) => supertype.isDartCoreObject);
// We should have at most one non-trivial supertype.
assert(immediateSupertypes.length <= 1);
for (final supertype in immediateSupertypes) {
if (_isInJsTypesOrJsInterop(supertype.element)) {
parentJsType = "'${supertype.element.name}'";
}
}
// Ensure that the hierarchy forms a tree.
assert((parentJsType == null) == (name == 'JSAny'));
jsTypeSupertypes["'$name'"] = parentJsType;
}
if (element is TypeAliasElement) {
final type = element.aliasedType;
if (type is InterfaceType) storeSupertypes(type.element);
} else if (element is InterfaceElement) {
storeSupertypes(element);
}
}
}
final jsTypeSupertypesScript = '''
// Copyright (c) 2023, 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.
// Updated by $_thisScript. Do not modify by hand.
const Map<String, String?> jsTypeSupertypes = {
${jsTypeSupertypes.entries.map((e) => " ${e.key}: ${e.value},").join('\n')}
};
''';
final jsTypeSupertypesPath =
p.join(_bindingsGeneratorPath, 'js_type_supertypes.dart');
await File(jsTypeSupertypesPath).writeAsString(jsTypeSupertypesScript);
}
final _usage = '''
Usage:
${_parser.usage}''';
final _parser = ArgParser()
..addFlag('update', abbr: 'u', help: 'Update npm dependencies')
..addFlag('compile', defaultsTo: true)
..addFlag('help', negatable: false);