[pub_formats] Generate `pubspec.lock` API (#2427)
diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json
index a1b2f1c..d80ed7a 100644
--- a/.github/pr-title-checker-config.json
+++ b/.github/pr-title-checker-config.json
@@ -15,6 +15,7 @@
"[native_test_helpers] ",
"[native_toolchain_c] ",
"[objective_c] ",
+ "[pub_formats] ",
"[swift2objc] ",
"[swiftgen] "
],
diff --git a/pkgs/pub_formats/doc/schema/pubspec_lock.schema.json b/pkgs/pub_formats/doc/schema/pubspec_lock.schema.json
new file mode 100644
index 0000000..0b179f6
--- /dev/null
+++ b/pkgs/pub_formats/doc/schema/pubspec_lock.schema.json
@@ -0,0 +1,196 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Pubspec Lock File Schema",
+ "description": "Schema for a Dart pubspec.lock file. Note that this is reverse engineered and not the source of truth for pubspec locks.",
+ "$ref": "#/definitions/PubspecLockFile",
+ "definitions": {
+ "PubspecLockFile": {
+ "type": "object",
+ "required": [
+ "sdks"
+ ],
+ "properties": {
+ "packages": {
+ "type": "object",
+ "description": "Details of the locked packages.",
+ "additionalProperties": {
+ "$ref": "#/definitions/Package"
+ }
+ },
+ "sdks": {
+ "$ref": "#/definitions/SDKs"
+ }
+ },
+ "additionalProperties": false
+ },
+ "Package": {
+ "type": "object",
+ "required": [
+ "dependency",
+ "description",
+ "source",
+ "version"
+ ],
+ "properties": {
+ "dependency": {
+ "$ref": "#/definitions/DependencyType"
+ },
+ "description": {
+ "$ref": "#/definitions/PackageDescription"
+ },
+ "source": {
+ "$ref": "#/definitions/PackageSource"
+ },
+ "version": {
+ "$ref": "#/definitions/PackageVersion"
+ }
+ },
+ "additionalProperties": false
+ },
+ "DependencyType": {
+ "type": "string",
+ "description": "The type of dependency.",
+ "anyOf": [
+ {
+ "enum": [
+ "transitive",
+ "direct main"
+ ]
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "PackageDescription": {
+ "type": "object",
+ "description": "Description of the package source."
+ },
+ "HostedPackageDescription": {
+ "description": "For hosted packages.",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PackageDescription"
+ }
+ ],
+ "required": [
+ "name",
+ "sha256",
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the package."
+ },
+ "sha256": {
+ "type": "string",
+ "description": "SHA256 checksum of the package."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL of the package host."
+ }
+ },
+ "additionalProperties": false
+ },
+ "GitPackageDescription": {
+ "description": "For git packages.",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PackageDescription"
+ }
+ ],
+ "required": [
+ "path",
+ "ref",
+ "resolved-ref",
+ "url"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Path within the git repository (if applicable)."
+ },
+ "ref": {
+ "type": "string",
+ "description": "Git reference (e.g., branch, tag, or commit hash)."
+ },
+ "resolved-ref": {
+ "type": "string",
+ "description": "Resolved git commit hash."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL of the git repository."
+ }
+ },
+ "additionalProperties": false
+ },
+ "PathPackageDescription": {
+ "description": "For path packages.",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PackageDescription"
+ }
+ ],
+ "required": [
+ "path",
+ "relative"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Absolute or relative path to the package."
+ },
+ "relative": {
+ "type": "boolean",
+ "description": "Indicates if the path is relative to the lockfile."
+ }
+ },
+ "additionalProperties": false
+ },
+ "PackageSource": {
+ "type": "string",
+ "description": "The source of the package.",
+ "anyOf": [
+ {
+ "enum": [
+ "hosted",
+ "git",
+ "path"
+ ]
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "PackageVersion": {
+ "type": "string",
+ "description": "The locked version of the package."
+ },
+ "SDKs": {
+ "type": "object",
+ "description": "Details of the SDKs used.",
+ "required": [
+ "dart"
+ ],
+ "properties": {
+ "dart": {
+ "$ref": "#/definitions/DartSDKVersion"
+ }
+ },
+ "additionalProperties": false
+ },
+ "DartSDKVersion": {
+ "type": "string",
+ "description": "The Dart SDK version constraint."
+ }
+ }
+}
diff --git a/pkgs/pub_formats/lib/pubspec_formats.dart b/pkgs/pub_formats/lib/pubspec_formats.dart
new file mode 100644
index 0000000..2f5926c
--- /dev/null
+++ b/pkgs/pub_formats/lib/pubspec_formats.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2025, 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.
+
+/// An internal library containing file formats for the pub client. This library
+/// is not meant to have a stable API and might break at any point when
+/// `package:json_syntax_generator` is updated. This API is used in the Dart
+/// SDK, so when doing breaking changes please roll ASAP to the Dart SDK.
+library;
+
+export 'src/pubspec_lock_syntax.g.dart';
diff --git a/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart b/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart
new file mode 100644
index 0000000..b99fb4b
--- /dev/null
+++ b/pkgs/pub_formats/lib/src/pubspec_lock_syntax.g.dart
@@ -0,0 +1,712 @@
+// Copyright (c) 2025, 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.
+
+// This file is generated, do not edit.
+// File generated by pkgs/pub_formats/tool/generate.dart.
+// Must be rerun when pkgs/pub_formats/doc/schema/ is modified.
+
+// ignore_for_file: unused_element, public_member_api_docs
+
+import 'dart:io';
+
+class DependencyTypeSyntax {
+ final String name;
+
+ const DependencyTypeSyntax._(this.name);
+
+ static const directMain = DependencyTypeSyntax._('direct main');
+
+ static const transitive = DependencyTypeSyntax._('transitive');
+
+ static const List<DependencyTypeSyntax> values = [directMain, transitive];
+
+ static final Map<String, DependencyTypeSyntax> _byName = {
+ for (final value in values) value.name: value,
+ };
+
+ DependencyTypeSyntax.unknown(this.name)
+ : assert(!_byName.keys.contains(name));
+
+ factory DependencyTypeSyntax.fromJson(String name) {
+ final knownValue = _byName[name];
+ if (knownValue != null) {
+ return knownValue;
+ }
+ return DependencyTypeSyntax.unknown(name);
+ }
+
+ bool get isKnown => _byName[name] != null;
+
+ @override
+ String toString() => name;
+}
+
+class GitPackageDescriptionSyntax extends PackageDescriptionSyntax {
+ GitPackageDescriptionSyntax.fromJson(super.json, {super.path})
+ : super.fromJson();
+
+ GitPackageDescriptionSyntax({
+ required String path$,
+ required String ref,
+ required String resolvedRef,
+ required String url,
+ }) : super() {
+ _path$ = path$;
+ _ref = ref;
+ _resolvedRef = resolvedRef;
+ _url = url;
+ json.sortOnKey();
+ }
+
+ /// Setup all fields for [GitPackageDescriptionSyntax] that are not in
+ /// [PackageDescriptionSyntax].
+ void setup({
+ required String path$,
+ required String ref,
+ required String resolvedRef,
+ required String url,
+ }) {
+ _path$ = path$;
+ _ref = ref;
+ _resolvedRef = resolvedRef;
+ _url = url;
+ json.sortOnKey();
+ }
+
+ String get path$ => _reader.get<String>('path');
+
+ set _path$(String value) {
+ json.setOrRemove('path', value);
+ }
+
+ List<String> _validatePath$() => _reader.validate<String>('path');
+
+ String get ref => _reader.get<String>('ref');
+
+ set _ref(String value) {
+ json.setOrRemove('ref', value);
+ }
+
+ List<String> _validateRef() => _reader.validate<String>('ref');
+
+ String get resolvedRef => _reader.get<String>('resolved-ref');
+
+ set _resolvedRef(String value) {
+ json.setOrRemove('resolved-ref', value);
+ }
+
+ List<String> _validateResolvedRef() =>
+ _reader.validate<String>('resolved-ref');
+
+ String get url => _reader.get<String>('url');
+
+ set _url(String value) {
+ json.setOrRemove('url', value);
+ }
+
+ List<String> _validateUrl() => _reader.validate<String>('url');
+
+ @override
+ List<String> validate() => [
+ ...super.validate(),
+ ..._validatePath$(),
+ ..._validateRef(),
+ ..._validateResolvedRef(),
+ ..._validateUrl(),
+ ];
+
+ @override
+ String toString() => 'GitPackageDescriptionSyntax($json)';
+}
+
+class HostedPackageDescriptionSyntax extends PackageDescriptionSyntax {
+ HostedPackageDescriptionSyntax.fromJson(super.json, {super.path})
+ : super.fromJson();
+
+ HostedPackageDescriptionSyntax({
+ required String name,
+ required String sha256,
+ required String url,
+ }) : super() {
+ _name = name;
+ _sha256 = sha256;
+ _url = url;
+ json.sortOnKey();
+ }
+
+ /// Setup all fields for [HostedPackageDescriptionSyntax] that are not in
+ /// [PackageDescriptionSyntax].
+ void setup({
+ required String name,
+ required String sha256,
+ required String url,
+ }) {
+ _name = name;
+ _sha256 = sha256;
+ _url = url;
+ json.sortOnKey();
+ }
+
+ String get name => _reader.get<String>('name');
+
+ set _name(String value) {
+ json.setOrRemove('name', value);
+ }
+
+ List<String> _validateName() => _reader.validate<String>('name');
+
+ String get sha256 => _reader.get<String>('sha256');
+
+ set _sha256(String value) {
+ json.setOrRemove('sha256', value);
+ }
+
+ List<String> _validateSha256() => _reader.validate<String>('sha256');
+
+ String get url => _reader.get<String>('url');
+
+ set _url(String value) {
+ json.setOrRemove('url', value);
+ }
+
+ List<String> _validateUrl() => _reader.validate<String>('url');
+
+ @override
+ List<String> validate() => [
+ ...super.validate(),
+ ..._validateName(),
+ ..._validateSha256(),
+ ..._validateUrl(),
+ ];
+
+ @override
+ String toString() => 'HostedPackageDescriptionSyntax($json)';
+}
+
+class PackageSyntax extends JsonObjectSyntax {
+ PackageSyntax.fromJson(super.json, {super.path = const []})
+ : super.fromJson();
+
+ PackageSyntax({
+ required DependencyTypeSyntax dependency,
+ required PackageDescriptionSyntax description,
+ required PackageSourceSyntax source,
+ required String version,
+ }) : super() {
+ _dependency = dependency;
+ _description = description;
+ _source = source;
+ _version = version;
+ json.sortOnKey();
+ }
+
+ DependencyTypeSyntax get dependency {
+ final jsonValue = _reader.get<String>('dependency');
+ return DependencyTypeSyntax.fromJson(jsonValue);
+ }
+
+ set _dependency(DependencyTypeSyntax value) {
+ json['dependency'] = value.name;
+ }
+
+ List<String> _validateDependency() => _reader.validate<String>('dependency');
+
+ PackageDescriptionSyntax get description {
+ final jsonValue = _reader.map$('description');
+ return PackageDescriptionSyntax.fromJson(
+ jsonValue,
+ path: [...path, 'description'],
+ );
+ }
+
+ set _description(PackageDescriptionSyntax value) {
+ json['description'] = value.json;
+ }
+
+ List<String> _validateDescription() {
+ final mapErrors = _reader.validate<Map<String, Object?>>('description');
+ if (mapErrors.isNotEmpty) {
+ return mapErrors;
+ }
+ return description.validate();
+ }
+
+ PackageSourceSyntax get source {
+ final jsonValue = _reader.get<String>('source');
+ return PackageSourceSyntax.fromJson(jsonValue);
+ }
+
+ set _source(PackageSourceSyntax value) {
+ json['source'] = value.name;
+ }
+
+ List<String> _validateSource() => _reader.validate<String>('source');
+
+ String get version => _reader.get<String>('version');
+
+ set _version(String value) {
+ json.setOrRemove('version', value);
+ }
+
+ List<String> _validateVersion() => _reader.validate<String>('version');
+
+ @override
+ List<String> validate() => [
+ ...super.validate(),
+ ..._validateDependency(),
+ ..._validateDescription(),
+ ..._validateSource(),
+ ..._validateVersion(),
+ ];
+
+ @override
+ String toString() => 'PackageSyntax($json)';
+}
+
+class PackageDescriptionSyntax extends JsonObjectSyntax {
+ PackageDescriptionSyntax.fromJson(super.json, {super.path = const []})
+ : super.fromJson();
+
+ PackageDescriptionSyntax() : super();
+
+ @override
+ List<String> validate() => [...super.validate()];
+
+ @override
+ String toString() => 'PackageDescriptionSyntax($json)';
+}
+
+class PackageSourceSyntax {
+ final String name;
+
+ const PackageSourceSyntax._(this.name);
+
+ static const git = PackageSourceSyntax._('git');
+
+ static const hosted = PackageSourceSyntax._('hosted');
+
+ static const path$ = PackageSourceSyntax._('path');
+
+ static const List<PackageSourceSyntax> values = [git, hosted, path$];
+
+ static final Map<String, PackageSourceSyntax> _byName = {
+ for (final value in values) value.name: value,
+ };
+
+ PackageSourceSyntax.unknown(this.name) : assert(!_byName.keys.contains(name));
+
+ factory PackageSourceSyntax.fromJson(String name) {
+ final knownValue = _byName[name];
+ if (knownValue != null) {
+ return knownValue;
+ }
+ return PackageSourceSyntax.unknown(name);
+ }
+
+ bool get isKnown => _byName[name] != null;
+
+ @override
+ String toString() => name;
+}
+
+class PathPackageDescriptionSyntax extends PackageDescriptionSyntax {
+ PathPackageDescriptionSyntax.fromJson(super.json, {super.path})
+ : super.fromJson();
+
+ PathPackageDescriptionSyntax({required String path$, required bool relative})
+ : super() {
+ _path$ = path$;
+ _relative = relative;
+ json.sortOnKey();
+ }
+
+ /// Setup all fields for [PathPackageDescriptionSyntax] that are not in
+ /// [PackageDescriptionSyntax].
+ void setup({required String path$, required bool relative}) {
+ _path$ = path$;
+ _relative = relative;
+ json.sortOnKey();
+ }
+
+ String get path$ => _reader.get<String>('path');
+
+ set _path$(String value) {
+ json.setOrRemove('path', value);
+ }
+
+ List<String> _validatePath$() => _reader.validate<String>('path');
+
+ bool get relative => _reader.get<bool>('relative');
+
+ set _relative(bool value) {
+ json.setOrRemove('relative', value);
+ }
+
+ List<String> _validateRelative() => _reader.validate<bool>('relative');
+
+ @override
+ List<String> validate() => [
+ ...super.validate(),
+ ..._validatePath$(),
+ ..._validateRelative(),
+ ];
+
+ @override
+ String toString() => 'PathPackageDescriptionSyntax($json)';
+}
+
+class PubspecLockFileSyntax extends JsonObjectSyntax {
+ PubspecLockFileSyntax.fromJson(super.json, {super.path = const []})
+ : super.fromJson();
+
+ PubspecLockFileSyntax({
+ required Map<String, PackageSyntax>? packages,
+ required SDKsSyntax sdks,
+ }) : super() {
+ _packages = packages;
+ _sdks = sdks;
+ json.sortOnKey();
+ }
+
+ Map<String, PackageSyntax>? get packages {
+ final jsonValue = _reader.optionalMap('packages');
+ if (jsonValue == null) {
+ return null;
+ }
+ return {
+ for (final MapEntry(:key, :value) in jsonValue.entries)
+ key: PackageSyntax.fromJson(
+ value as Map<String, Object?>,
+ path: [...path, key],
+ ),
+ };
+ }
+
+ set _packages(Map<String, PackageSyntax>? value) {
+ if (value == null) {
+ json.remove('packages');
+ } else {
+ json['packages'] = {
+ for (final MapEntry(:key, :value) in value.entries) key: value.json,
+ };
+ }
+ }
+
+ List<String> _validatePackages() {
+ final mapErrors = _reader.validateOptionalMap('packages');
+ if (mapErrors.isNotEmpty) {
+ return mapErrors;
+ }
+ final jsonValue = _reader.optionalMap('packages');
+ if (jsonValue == null) {
+ return [];
+ }
+ final result = <String>[];
+ for (final value in packages!.values) {
+ result.addAll(value.validate());
+ }
+ return result;
+ }
+
+ SDKsSyntax get sdks {
+ final jsonValue = _reader.map$('sdks');
+ return SDKsSyntax.fromJson(jsonValue, path: [...path, 'sdks']);
+ }
+
+ set _sdks(SDKsSyntax value) {
+ json['sdks'] = value.json;
+ }
+
+ List<String> _validateSdks() {
+ final mapErrors = _reader.validate<Map<String, Object?>>('sdks');
+ if (mapErrors.isNotEmpty) {
+ return mapErrors;
+ }
+ return sdks.validate();
+ }
+
+ @override
+ List<String> validate() => [
+ ...super.validate(),
+ ..._validatePackages(),
+ ..._validateSdks(),
+ ];
+
+ @override
+ String toString() => 'PubspecLockFileSyntax($json)';
+}
+
+class SDKsSyntax extends JsonObjectSyntax {
+ SDKsSyntax.fromJson(super.json, {super.path = const []}) : super.fromJson();
+
+ SDKsSyntax({required String dart}) : super() {
+ _dart = dart;
+ json.sortOnKey();
+ }
+
+ String get dart => _reader.get<String>('dart');
+
+ set _dart(String value) {
+ json.setOrRemove('dart', value);
+ }
+
+ List<String> _validateDart() => _reader.validate<String>('dart');
+
+ @override
+ List<String> validate() => [...super.validate(), ..._validateDart()];
+
+ @override
+ String toString() => 'SDKsSyntax($json)';
+}
+
+class JsonObjectSyntax {
+ final Map<String, Object?> json;
+
+ final List<Object> path;
+
+ JsonReader get _reader => JsonReader(json, path);
+
+ JsonObjectSyntax() : json = {}, path = const [];
+
+ JsonObjectSyntax.fromJson(this.json, {this.path = const []});
+
+ List<String> validate() => [];
+}
+
+class JsonReader {
+ /// The JSON Object this reader is reading.
+ final Map<String, Object?> json;
+
+ /// The path traversed by readers of the surrounding JSON.
+ ///
+ /// Contains [String] property keys and [int] indices.
+ ///
+ /// This is used to give more precise error messages.
+ final List<Object> path;
+
+ JsonReader(this.json, this.path);
+
+ T get<T extends Object?>(String key) {
+ final value = json[key];
+ if (value is T) return value;
+ throwFormatException(value, T, [key]);
+ }
+
+ List<String> validate<T extends Object?>(String key) {
+ final value = json[key];
+ if (value is T) return [];
+ return [
+ errorString(value, T, [key]),
+ ];
+ }
+
+ List<T> list<T extends Object?>(String key) =>
+ _castList<T>(get<List<Object?>>(key), key);
+
+ List<String> validateList<T extends Object?>(String key) {
+ final listErrors = validate<List<Object?>>(key);
+ if (listErrors.isNotEmpty) {
+ return listErrors;
+ }
+ return _validateListElements(get<List<Object?>>(key), key);
+ }
+
+ List<T>? optionalList<T extends Object?>(String key) =>
+ switch (get<List<Object?>?>(key)?.cast<T>()) {
+ null => null,
+ final l => _castList<T>(l, key),
+ };
+
+ List<String> validateOptionalList<T extends Object?>(String key) {
+ final listErrors = validate<List<Object?>?>(key);
+ if (listErrors.isNotEmpty) {
+ return listErrors;
+ }
+ final list = get<List<Object?>?>(key);
+ if (list == null) {
+ return [];
+ }
+ return _validateListElements(list, key);
+ }
+
+ /// [List.cast] but with [FormatException]s.
+ List<T> _castList<T extends Object?>(List<Object?> list, String key) {
+ for (final (index, value) in list.indexed) {
+ if (value is! T) {
+ throwFormatException(value, T, [key, index]);
+ }
+ }
+ return list.cast();
+ }
+
+ List<String> _validateListElements<T extends Object?>(
+ List<Object?> list,
+ String key,
+ ) {
+ final result = <String>[];
+ for (final (index, value) in list.indexed) {
+ if (value is! T) {
+ result.add(errorString(value, T, [key, index]));
+ }
+ }
+ return result;
+ }
+
+ Map<String, T> map$<T extends Object?>(String key) =>
+ _castMap<T>(get<Map<String, Object?>>(key), key);
+
+ List<String> validateMap<T extends Object?>(String key) {
+ final mapErrors = validate<Map<String, Object?>>(key);
+ if (mapErrors.isNotEmpty) {
+ return mapErrors;
+ }
+ return _validateMapElements<T>(get<Map<String, Object?>>(key), key);
+ }
+
+ Map<String, T>? optionalMap<T extends Object?>(String key) =>
+ switch (get<Map<String, Object?>?>(key)) {
+ null => null,
+ final m => _castMap<T>(m, key),
+ };
+
+ List<String> validateOptionalMap<T extends Object?>(String key) {
+ final mapErrors = validate<Map<String, Object?>?>(key);
+ if (mapErrors.isNotEmpty) {
+ return mapErrors;
+ }
+ final map = get<Map<String, Object?>?>(key);
+ if (map == null) {
+ return [];
+ }
+ return _validateMapElements<T>(map, key);
+ }
+
+ /// [Map.cast] but with [FormatException]s.
+ Map<String, T> _castMap<T extends Object?>(
+ Map<String, Object?> map_,
+ String parentKey,
+ ) {
+ for (final MapEntry(:key, :value) in map_.entries) {
+ if (value is! T) {
+ throwFormatException(value, T, [parentKey, key]);
+ }
+ }
+ return map_.cast();
+ }
+
+ List<String> _validateMapElements<T extends Object?>(
+ Map<String, Object?> map_,
+ String parentKey,
+ ) {
+ final result = <String>[];
+ for (final MapEntry(:key, :value) in map_.entries) {
+ if (value is! T) {
+ result.add(errorString(value, T, [parentKey, key]));
+ }
+ }
+ return result;
+ }
+
+ List<String>? optionalStringList(String key) => optionalList<String>(key);
+
+ List<String> validateOptionalStringList(String key) =>
+ validateOptionalList<String>(key);
+
+ List<String> stringList(String key) => list<String>(key);
+
+ List<String> validateStringList(String key) => validateList<String>(key);
+
+ Uri path$(String key) => _fileSystemPathToUri(get<String>(key));
+
+ List<String> validatePath(String key) => validate<String>(key);
+
+ Uri? optionalPath(String key) {
+ final value = get<String?>(key);
+ if (value == null) return null;
+ return _fileSystemPathToUri(value);
+ }
+
+ List<String> validateOptionalPath(String key) => validate<String?>(key);
+
+ List<Uri>? optionalPathList(String key) {
+ final strings = optionalStringList(key);
+ if (strings == null) {
+ return null;
+ }
+ return [for (final string in strings) _fileSystemPathToUri(string)];
+ }
+
+ List<String> validateOptionalPathList(String key) =>
+ validateOptionalStringList(key);
+
+ static Uri _fileSystemPathToUri(String path) {
+ if (path.endsWith(Platform.pathSeparator)) {
+ return Uri.directory(path);
+ }
+ return Uri.file(path);
+ }
+
+ String _jsonPathToString(List<Object> pathEnding) =>
+ [...path, ...pathEnding].join('.');
+
+ Never throwFormatException(
+ Object? value,
+ Type expectedType,
+ List<Object> pathExtension,
+ ) {
+ throw FormatException(errorString(value, expectedType, pathExtension));
+ }
+
+ String errorString(
+ Object? value,
+ Type expectedType,
+ List<Object> pathExtension,
+ ) {
+ final pathString = _jsonPathToString(pathExtension);
+ if (value == null) {
+ return "No value was provided for '$pathString'."
+ ' Expected a $expectedType.';
+ }
+ return "Unexpected value '$value' (${value.runtimeType}) for '$pathString'."
+ ' Expected a $expectedType.';
+ }
+
+ /// Traverses a JSON path, returns `null` if the path cannot be traversed.
+ Object? tryTraverse(List<String> path) {
+ Object? json = this.json;
+ for (final key in path) {
+ if (json is! Map<String, Object?>) {
+ return null;
+ }
+ json = json[key];
+ }
+ return json;
+ }
+}
+
+extension on Map<String, Object?> {
+ void setOrRemove(String key, Object? value) {
+ if (value == null) {
+ remove(key);
+ } else {
+ this[key] = value;
+ }
+ }
+}
+
+extension on List<Uri> {
+ List<String> toJson() => [for (final uri in this) uri.toFilePath()];
+}
+
+extension<K extends Comparable<K>, V extends Object?> on Map<K, V> {
+ void sortOnKey() {
+ final result = <K, V>{};
+ final keysSorted = keys.toList()..sort();
+ for (final key in keysSorted) {
+ result[key] = this[key] as V;
+ }
+ clear();
+ addAll(result);
+ }
+}
diff --git a/pkgs/pub_formats/pubspec.yaml b/pkgs/pub_formats/pubspec.yaml
new file mode 100644
index 0000000..973cd26
--- /dev/null
+++ b/pkgs/pub_formats/pubspec.yaml
@@ -0,0 +1,26 @@
+name: pub_formats
+description: >-
+ An internal library containing file formats for the pub client. This library
+ is not meant to have a stable API and might break at any point when
+ `package:json_syntax_generator` is updated. This API is used in the Dart SDK,
+ so when doing breaking changes please roll ASAP to the Dart SDK.
+
+# This library is not meant to be published. It is used only in the Dart SDK.
+publish_to: none
+
+version: 0.0.1-wip
+
+resolution: workspace
+
+environment:
+ sdk: '>=3.9.0-21.0.dev <4.0.0'
+
+dev_dependencies:
+ args: ^2.6.0
+ json_schema: ^5.2.0 # May only be used in tool/ and test/json_schema/.
+ json_syntax_generator:
+ path: ../json_syntax_generator/
+ native_test_helpers:
+ path: ../native_test_helpers/
+ test: ^1.25.15
+ yaml: ^3.1.3
diff --git a/pkgs/pub_formats/test/pubspec_lock_test.dart b/pkgs/pub_formats/test/pubspec_lock_test.dart
new file mode 100644
index 0000000..3ddfa43
--- /dev/null
+++ b/pkgs/pub_formats/test/pubspec_lock_test.dart
@@ -0,0 +1,54 @@
+// Copyright (c) 2025, 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:io';
+
+import 'package:native_test_helpers/native_test_helpers.dart';
+import 'package:pub_formats/pubspec_formats.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+void main() {
+ test('pubspec lock', () {
+ final packageRoot = findPackageRoot('pub_formats');
+ final pubspecFile = File.fromUri(
+ packageRoot.resolve('test_data/pubspec_lock_1.yaml'),
+ );
+ final json = _convertYamlMapToJsonMap(
+ loadYaml(pubspecFile.readAsStringSync()),
+ );
+ final parsed = PubspecLockFileSyntax.fromJson(json);
+ final package = parsed.packages!['dart_apitool']!;
+ expect(package.source, PackageSourceSyntax.hosted);
+
+ final description = HostedPackageDescriptionSyntax.fromJson(
+ package.description.json,
+ );
+ expect(description.sha256, isNotEmpty);
+ });
+}
+
+Map<String, Object?> _convertYamlMapToJsonMap(YamlMap yamlMap) {
+ final Map<String, Object?> jsonMap = {};
+ yamlMap.forEach((key, value) {
+ if (key is! String) {
+ // Handle non-string keys if your YAML allows them, or throw an error.
+ // For typical JSON conversion, keys are expected to be strings.
+ throw ArgumentError('YAML map keys must be strings for JSON conversion.');
+ }
+ jsonMap[key] = _convertYamlValue(value);
+ });
+ return jsonMap;
+}
+
+Object? _convertYamlValue(dynamic yamlValue) {
+ if (yamlValue is YamlMap) {
+ return _convertYamlMapToJsonMap(yamlValue);
+ } else if (yamlValue is YamlList) {
+ return yamlValue.map((e) => _convertYamlValue(e)).toList();
+ } else {
+ // For primitive types (String, int, double, bool, null)
+ return yamlValue;
+ }
+}
diff --git a/pkgs/pub_formats/test_data/pubspec_lock_1.yaml b/pkgs/pub_formats/test_data/pubspec_lock_1.yaml
new file mode 100644
index 0000000..f8d75c1
--- /dev/null
+++ b/pkgs/pub_formats/test_data/pubspec_lock_1.yaml
@@ -0,0 +1,349 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
+ url: 'https://pub.dev'
+ source: hosted
+ version: '85.0.0'
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ sha256: b1ade5707ab7a90dfd519eaac78a7184341d19adb6096c68d499b59c7c6cf880
+ url: 'https://pub.dev'
+ source: hosted
+ version: '7.7.0'
+ ansicolor:
+ dependency: transitive
+ description:
+ name: ansicolor
+ sha256: '50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.0.3'
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: '2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '4.0.7'
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.7.0'
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: '758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.13.0'
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.4.1'
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: '959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.0.4'
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: '2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.19.1'
+ colorize:
+ dependency: transitive
+ description:
+ name: colorize
+ sha256: '584746cd6ba1cba0633b6720f494fe6f9601c4170f0666c1579d2aa2a61071ba'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '3.0.0'
+ colorize_lumberdash:
+ dependency: transitive
+ description:
+ name: colorize_lumberdash
+ sha256: '6069ad908445caab046e93738c4f941536b6266076f2998b4ac6c012355eb1ba'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '3.0.0'
+ console:
+ dependency: transitive
+ description:
+ name: console
+ sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a
+ url: 'https://pub.dev'
+ source: hosted
+ version: '4.1.0'
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
+ url: 'https://pub.dev'
+ source: hosted
+ version: '3.1.2'
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: '1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '3.0.6'
+ dart_apitool:
+ dependency: 'direct main'
+ description:
+ name: dart_apitool
+ sha256: '8950232e78406e5734d0dee9e61ec3c20092279d985dca17be7382608df7b67b'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '0.21.1'
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ sha256: '289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.1.4'
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+ url: 'https://pub.dev'
+ source: hosted
+ version: '7.0.1'
+ freezed_annotation:
+ dependency: transitive
+ description:
+ name: freezed_annotation
+ sha256: '7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '3.1.0'
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.1.3'
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: '1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '4.9.0'
+ lumberdash:
+ dependency: transitive
+ description:
+ name: lumberdash
+ sha256: '1bc3750c094adb7f213a61883ca9878ba052fde52d9974b9039598c651fe096c'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '3.0.0'
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: '23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.17.0'
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.2.0'
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: '75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.9.1'
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: '9436fe11f82d7cc1642a8671e5aa4149ffa9ae9116e6cf6dd665fc0653e3825c'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '7.0.0'
+ plist_parser:
+ dependency: transitive
+ description:
+ name: plist_parser
+ sha256: e2a6f9abfa0c45c0253656b7360abb0dfb84af9937bace74605b93d2aad2bf0c
+ url: 'https://pub.dev'
+ source: hosted
+ version: '0.0.11'
+ posix:
+ dependency: transitive
+ description:
+ name: posix
+ sha256: '6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '6.0.3'
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: '5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.2.0'
+ pubspec_manager:
+ dependency: transitive
+ description:
+ name: pubspec_manager
+ sha256: '416609635f6c53ab759223ae66e71282e61a863c13ec47d60381e4b8046c2a71'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.0.3'
+ pubspec_parse:
+ dependency: transitive
+ description:
+ name: pubspec_parse
+ sha256: '0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.5.0'
+ simple_sparse_list:
+ dependency: transitive
+ description:
+ name: simple_sparse_list
+ sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f
+ url: 'https://pub.dev'
+ source: hosted
+ version: '0.1.4'
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: '254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.10.1'
+ stack:
+ dependency: transitive
+ description:
+ name: stack
+ sha256: f5a3032c965b74f394dc6aa138c82373a3403438c68cf5d8e6ef2bc2698949bf
+ url: 'https://pub.dev'
+ source: hosted
+ version: '0.2.2'
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: '921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.4.1'
+ strings:
+ dependency: transitive
+ description:
+ name: strings
+ sha256: ff7373d0221207fe3b834953775c0e6a492b37f4409a3d539d35a34a7f50c38b
+ url: 'https://pub.dev'
+ source: hosted
+ version: '4.0.0'
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: '7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.2.2'
+ tuple:
+ dependency: transitive
+ description:
+ name: tuple
+ sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.0.2'
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.4.0'
+ unicode:
+ dependency: transitive
+ description:
+ name: unicode
+ sha256: '0d99edbd2e74726bed2e4989713c8bec02e5581628e334d8c88c0271593fb402'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.1.8'
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ url: 'https://pub.dev'
+ source: hosted
+ version: '2.2.0'
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ sha256: '0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '1.1.2'
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: '3202a47961c1a0af6097c9f8c1b492d705248ba309e6f7a72410422c05046851'
+ url: 'https://pub.dev'
+ source: hosted
+ version: '6.6.0'
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+ url: 'https://pub.dev'
+ source: hosted
+ version: '3.1.3'
+sdks:
+ dart: '>=3.9.0-293.0.dev <4.0.0'
diff --git a/pkgs/pub_formats/tool/generate.dart b/pkgs/pub_formats/tool/generate.dart
new file mode 100644
index 0000000..3f0c186
--- /dev/null
+++ b/pkgs/pub_formats/tool/generate.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2025, 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';
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:json_schema/json_schema.dart';
+import 'package:json_syntax_generator/json_syntax_generator.dart';
+import 'package:native_test_helpers/native_test_helpers.dart';
+
+void main(List<String> args) {
+ final packageRoot = findPackageRoot('pub_formats');
+
+ final stopwatch = Stopwatch()..start();
+ final parser = ArgParser()
+ ..addFlag(
+ 'set-exit-if-changed',
+ negatable: false,
+ help: 'Return a non-zero exit code if any files were changed.',
+ )
+ ..addFlag(
+ 'dump',
+ abbr: 'd',
+ negatable: false,
+ help: 'Dump analyzed schema to .g.txt file.',
+ );
+ final argResults = parser.parse(args);
+
+ final setExitIfChanged = argResults['set-exit-if-changed'] as bool;
+ final dumpAnalyzedSchema = argResults['dump'] as bool;
+ var generatedCount = 0;
+ var changedCount = 0;
+
+ final schemaFile = File.fromUri(
+ packageRoot.resolve('doc/schema/pubspec_lock.schema.json'),
+ );
+ final schemaJson = jsonDecode(schemaFile.readAsStringSync());
+ final schema = JsonSchema.create(schemaJson);
+
+ final analyzedSchema = SchemaAnalyzer(
+ schema,
+ nameOverrides: {'path': 'path\$'},
+ ).analyze();
+ final textDumpFile = File.fromUri(
+ packageRoot.resolve('lib/src/pubspec_lock_syntax.g.txt'),
+ );
+ final newTextDumpContent = analyzedSchema.toString();
+ if (dumpAnalyzedSchema) {
+ var oldTextDumpContent = '';
+ if (textDumpFile.existsSync()) {
+ oldTextDumpContent = textDumpFile.readAsStringSync();
+ }
+ if (oldTextDumpContent != newTextDumpContent) {
+ textDumpFile.writeAsStringSync(newTextDumpContent);
+ print('Generated ${textDumpFile.uri}');
+ changedCount += 1;
+ }
+ generatedCount += 1;
+ } else if (textDumpFile.existsSync()) {
+ textDumpFile.deleteSync();
+ print('Deleted ${textDumpFile.uri}');
+ changedCount += 1;
+ }
+
+ final output = SyntaxGenerator(
+ analyzedSchema,
+ header: '''
+// This file is generated, do not edit.
+// File generated by pkgs/pub_formats/tool/generate.dart.
+// Must be rerun when pkgs/pub_formats/doc/schema/ is modified.
+''',
+ ).generate();
+ final outputUri = packageRoot.resolve('lib/src/pubspec_lock_syntax.g.dart');
+ final outputFile = File.fromUri(outputUri);
+ var oldOutputContent = '';
+ if (outputFile.existsSync()) {
+ oldOutputContent = outputFile.readAsStringSync();
+ }
+
+ if (oldOutputContent != output) {
+ outputFile.writeAsStringSync(output);
+ }
+
+ Process.runSync(Platform.executable, ['format', outputUri.toFilePath()]);
+
+ final newOutputContent = outputFile.readAsStringSync();
+
+ final newContentNormalized = newOutputContent.replaceAll('\r\n', '\n');
+ final oldContentNormalized = oldOutputContent.replaceAll('\r\n', '\n');
+ if (newContentNormalized != oldContentNormalized) {
+ print('Generated $outputUri');
+ changedCount += 1;
+ }
+ generatedCount += 1;
+
+ stopwatch.stop();
+ final duration = stopwatch.elapsedMilliseconds / 1000.0;
+ print(
+ 'Generated $generatedCount files ($changedCount changed) in '
+ '${duration.toStringAsFixed(2)} seconds.',
+ );
+ if (setExitIfChanged && changedCount > 0) {
+ exit(1);
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 3a68ee6..876bb7b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -58,6 +58,7 @@
- pkgs/hooks/example/link/package_with_assets
- pkgs/json_syntax_generator
- pkgs/native_test_helpers
+ - pkgs/pub_formats
- pkgs/native_toolchain_c
- pkgs/repo_lint_rules
diff --git a/tool/ci.dart b/tool/ci.dart
index 16dd954..d1d0267 100644
--- a/tool/ci.dart
+++ b/tool/ci.dart
@@ -58,6 +58,7 @@
'pkgs/hooks/tool/generate_schemas.dart',
'pkgs/hooks/tool/generate_syntax.dart',
'pkgs/hooks/tool/normalize.dart',
+ 'pkgs/pub_formats/tool/generate.dart',
];
for (final generator in generators) {
_runProcess('dart', [generator, '--set-exit-if-changed']);