From 393ed8301ceb1c0557dd1916c17d16c0a3a73003 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Fri, 2 Feb 2024 15:44:11 -0800 Subject: [PATCH] Parse browser compatibility metadata (#144) parse browser-compat-data --- tool/generator/bcd.dart | 122 +++++++++++++++++++++++++++++++ tool/generator/package-lock.json | 13 ++-- tool/generator/package.json | 2 +- tool/generator/translator.dart | 85 ++------------------- 4 files changed, 136 insertions(+), 86 deletions(-) create mode 100644 tool/generator/bcd.dart diff --git a/tool/generator/bcd.dart b/tool/generator/bcd.dart new file mode 100644 index 00000000..c9b5114f --- /dev/null +++ b/tool/generator/bcd.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2024, 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:convert' hide json; +import 'dart:js_interop'; + +import 'package:path/path.dart' as p; + +import 'filesystem_api.dart'; + +/// A class to read from the browser-compat-data files and parse interface and +/// property status (standards track, experimental, deprecated) and supported +/// browser (chrome, safari, firefox) info. +class BrowserCompatData { + static BrowserCompatData read() { + final path = + p.join('node_modules', '@mdn', 'browser-compat-data', 'data.json'); + final content = (fs.readFileSync( + path.toJS, + JSReadFileOptions(encoding: 'utf8'.toJS), + ) as JSString) + .toDart; + + final api = (jsonDecode(content) as Map)['api'] as Map; + final interfaces = api.symbolNames + .map((key) => BCDInterfaceStatus(key, api[key] as Map)) + .toList(); + return BrowserCompatData(Map.fromIterable( + interfaces, + key: (i) => (i as BCDInterfaceStatus).name, + )); + } + + final Map interfaces; + + BrowserCompatData(this.interfaces); + + BCDInterfaceStatus? retrieveInterfaceFor(String name) => interfaces[name]; +} + +class BCDInterfaceStatus extends BCDItem { + late final Map properties; + + BCDInterfaceStatus(super.name, super.json) { + properties = Map.fromIterable( + json.symbolNames, + value: (name) => BCDPropertyStatus( + name as String, json[name] as Map, this), + ); + } + + BCDPropertyStatus? retrievePropertyFor(String name) => properties[name]; +} + +class BCDPropertyStatus extends BCDItem { + final BCDInterfaceStatus parent; + + BCDPropertyStatus(super.name, super.json, this.parent); +} + +abstract class BCDItem { + final String name; + final Map json; + + BCDItem(this.name, this.json); + + Map get _compat => json['__compat'] as Map; + Map get _status => _compat['status'] as Map; + Map get _support => + _compat['support'] as Map; + + bool get deprecated => _status['deprecated'] as bool? ?? false; + bool get experimental => _status['experimental'] as bool? ?? false; + bool get standardTrack => _status['standard_track'] as bool? ?? false; + + List get status => [ + if (standardTrack) 'standards-track', + if (deprecated) 'deprecated', + if (experimental) 'experimental', + ]; + + String get _statusDescription => status.join(', '); + + bool get chromeSupported => _supportedInBrowser('chrome'); + bool get firefoxSupported => _supportedInBrowser('firefox'); + bool get safariSupported => _supportedInBrowser('safari'); + + List get browsers => [ + if (chromeSupported) 'chrome', + if (firefoxSupported) 'firefox', + if (safariSupported) 'safari', + ]; + + String get _browsersDescription => browsers.join(', '); + + int get browserCount => browsers.length; + + bool _supportedInBrowser(String browser) { + final map = (_support[browser] is List + ? (_support[browser] as List).first + : _support[browser]) as Map; + + if (map.containsKey('version_removed')) { + return false; + } + + final value = map['version_added']; + if (value is String) return true; + if (value is bool) return value; + return false; + } + + @override + String toString() => '$name ($_browsersDescription) [$_statusDescription]'; +} + +extension BCDJsonDataExtension on Map { + /// Return keys which coorespond to symbol names (i.e., filter out non-symbol + /// metadata (`__meta`, `__compat`, ...). + Iterable get symbolNames => keys.where((key) => !key.startsWith('_')); +} diff --git a/tool/generator/package-lock.json b/tool/generator/package-lock.json index 24089c0a..49a676ba 100644 --- a/tool/generator/package-lock.json +++ b/tool/generator/package-lock.json @@ -9,14 +9,19 @@ "version": "0.0.1", "license": "BSD 3", "dependencies": { + "@mdn/browser-compat-data": "^5.5.2", "@webref/css": "^6.10.0", "@webref/idl": "^3.23.0" }, "devDependencies": { - "web-specs": "^2.74.1", "webidl2": "^24.2.2" } }, + "node_modules/@mdn/browser-compat-data": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.5.6.tgz", + "integrity": "sha512-dZgfsA1v8r+8QBPh7YqPaNz9KRlLi/iyac1/mwbaIV7yFrtW1qCkmzBJiJGsMhI1/JdayJvC81lVS/UEzHsgbA==" + }, "node_modules/@webref/css": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/@webref/css/-/css-6.10.0.tgz", @@ -61,12 +66,6 @@ "node": ">=0.10.0" } }, - "node_modules/web-specs": { - "version": "2.74.1", - "resolved": "https://registry.npmjs.org/web-specs/-/web-specs-2.74.1.tgz", - "integrity": "sha512-OrXix5LVFhnJ1uvj00jy1vAOkUNzoVWKF7IqkTgqTI9CmYIdLxpknSvE/iqOOHUVNX90izUUj8lJVNjCdnWCuA==", - "dev": true - }, "node_modules/webidl2": { "version": "24.4.1", "resolved": "https://registry.npmjs.org/webidl2/-/webidl2-24.4.1.tgz", diff --git a/tool/generator/package.json b/tool/generator/package.json index 9b9b62da..01711313 100644 --- a/tool/generator/package.json +++ b/tool/generator/package.json @@ -9,11 +9,11 @@ "author": "Dart project authors", "license": "BSD 3", "dependencies": { + "@mdn/browser-compat-data": "^5.5.2", "@webref/css": "^6.10.0", "@webref/idl": "^3.23.0" }, "devDependencies": { - "web-specs": "^2.74.1", "webidl2": "^24.2.2" } } diff --git a/tool/generator/translator.dart b/tool/generator/translator.dart index 43a870df..a3a91ee4 100644 --- a/tool/generator/translator.dart +++ b/tool/generator/translator.dart @@ -2,14 +2,13 @@ // 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:convert'; import 'dart:js_interop'; import 'package:code_builder/code_builder.dart' as code; import 'package:path/path.dart' as p; import 'banned_names.dart'; -import 'filesystem_api.dart'; +import 'bcd.dart'; import 'singletons.dart'; import 'type_aliases.dart'; import 'type_union.dart'; @@ -486,7 +485,7 @@ class Translator { final _includes = []; late String _currentlyTranslatingUrl; - late WebSpecs webSpecs; + late BrowserCompatData browserCompatData; /// Singleton so that various helper methods can access info about the AST. static Translator? instance; @@ -494,7 +493,7 @@ class Translator { Translator( this.packageRoot, this._librarySubDir, this._cssStyleDeclarations) { instance = this; - webSpecs = WebSpecs.read(); + browserCompatData = BrowserCompatData.read(); } /// Set or update partial interfaces so we can have a unified interface @@ -539,10 +538,6 @@ class Translator { final libraryPath = '$_librarySubDir/${shortName.kebabToSnake}.dart'; assert(!_libraries.containsKey(libraryPath)); - // TODO: Use the info from the spec to skip generation of some libraries. - // ignore: unused_local_variable - final spec = webSpecs.specFor(shortName)!; - final library = _Library(this, '$packageRoot/$libraryPath'); _libraries[libraryPath] = library; @@ -839,6 +834,7 @@ class Translator { code.ExtensionType _extensionType({ required String jsName, required String dartClassName, + required BCDInterfaceStatus? interfaceStatus, required List implements, required _OverridableConstructor? constructor, required List<_OverridableOperation> operations, @@ -885,6 +881,8 @@ class Translator { // private classes, and make their first character uppercase in the process. final dartClassName = isNamespace ? '\$${capitalize(jsName)}' : jsName; + final status = browserCompatData.retrieveInterfaceFor(name); + // We create a getter for namespaces with the expected name. We also create // getters for a few pre-defined singleton classes. final getterName = isNamespace ? jsName : singletons[jsName]; @@ -903,6 +901,7 @@ class Translator { _extensionType( jsName: jsName, dartClassName: dartClassName, + interfaceStatus: status, implements: implements, constructor: interfacelike.constructor, operations: operations, @@ -965,73 +964,3 @@ class Translator { return dartLibraries; } } - -class WebSpecs { - static WebSpecs read() { - final path = p.join('node_modules', 'web-specs', 'index.json'); - final content = (fs.readFileSync( - path.toJS, - JSReadFileOptions(encoding: 'utf8'.toJS), - ) as JSString) - .toDart; - return WebSpecs( - (jsonDecode(content) as List) - .map((json) => WebSpec(json as Map)) - .toList(), - ); - } - - final List specs; - - WebSpecs(this.specs); - - WebSpec? specFor(String shortName) { - for (final spec in specs) { - if (spec.shortname == shortName) { - return spec; - } - } - - for (final spec in specs) { - if (spec.seriesShortname == shortName) { - return spec; - } - } - - return null; - } -} - -class WebSpec { - final Map json; - - WebSpec(this.json); - - String get url => json['url'] as String; - - String get shortname => json['shortname'] as String; - - String? get seriesShortname { - if (!json.containsKey('series')) return null; - return (json['series'] as Map)['shortname'] as String?; - } - - String get standing => json['standing'] as String; - - List get categories { - if (json.containsKey('categories')) { - return (json['categories'] as List).cast(); - } else { - return const []; - } - } - - String? get releaseStatus { - if (!json.containsKey('release')) return null; - return (json['release'] as Map)['status'] as String?; - } - - @override - String toString() => - '$shortname $url $standing [${categories.join(',')}] $releaseStatus'; -}