| // 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:native_test_helpers/native_test_helpers.dart'; |
| |
| import '../test/json_schema/helpers.dart'; |
| import 'normalize.dart'; |
| |
| void main(List<String> args) { |
| 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.', |
| ); |
| final argResults = parser.parse(args); |
| final setExitIfChanged = argResults['set-exit-if-changed'] as bool; |
| |
| final counts = Counts(); |
| |
| generateSharedDefinitions(counts); |
| generateEntryPoints(counts); |
| |
| stopwatch.stop(); |
| final duration = stopwatch.elapsedMilliseconds / 1000.0; |
| print( |
| 'Generated ${counts.generated} files (${counts.changed} changed) in ' |
| '${duration.toStringAsFixed(2)} seconds.', |
| ); |
| if (setExitIfChanged && counts.changed > 0) { |
| exit(1); |
| } |
| } |
| |
| class Counts { |
| int generated = 0; |
| int changed = 0; |
| } |
| |
| Uri packageUri = findPackageRoot('hooks'); |
| |
| const jsonEncoder = JsonEncoder.withIndent(' '); |
| |
| enum Party { hook, shared, sdk } |
| |
| enum Hook { build, hook, link } |
| |
| const packages = ['hooks', 'code_assets', 'data_assets']; |
| |
| void generateSharedDefinitions(Counts counts) { |
| const hookOutputAssetOverride = { |
| 'properties': { |
| 'assets': { |
| 'type': 'array', |
| 'items': {r'$ref': 'shared_definitions.schema.json#/definitions/Asset'}, |
| }, |
| }, |
| }; |
| const buildOutputAssetOverride = { |
| 'properties': { |
| 'assets_for_linking': { |
| 'type': 'object', |
| 'additionalProperties': { |
| 'type': 'array', |
| 'items': { |
| r'$ref': 'shared_definitions.schema.json#/definitions/Asset', |
| }, |
| }, |
| }, |
| 'assets_for_build': { |
| 'type': 'array', |
| 'items': {r'$ref': 'shared_definitions.schema.json#/definitions/Asset'}, |
| }, |
| }, |
| }; |
| const buildInputAssetOverride = { |
| 'properties': { |
| 'assets': { |
| 'type': 'object', |
| 'additionalProperties': { |
| 'type': 'array', |
| 'items': { |
| r'$ref': 'shared_definitions.schema.json#/definitions/Asset', |
| }, |
| }, |
| }, |
| }, |
| }; |
| const linkInputAssetOverride = { |
| 'properties': { |
| 'assets': { |
| 'type': 'array', |
| 'items': {r'$ref': 'shared_definitions.schema.json#/definitions/Asset'}, |
| }, |
| }, |
| }; |
| const hookInputCodeOverride = { |
| 'properties': { |
| 'config': {r'$ref': 'shared_definitions.schema.json#/definitions/Config'}, |
| }, |
| }; |
| const configOverride = { |
| 'Config': { |
| 'allOf': [ |
| { |
| r'$ref': |
| '../../../../hooks/doc/schema/shared/shared_definitions.schema.json#/definitions/Config', |
| }, |
| {r'$ref': 'shared_definitions.schema.json#/definitions/Config'}, |
| ], |
| }, |
| }; |
| |
| for (final package in packages) { |
| for (final party in Party.values) { |
| if (package == 'hooks' && party == Party.shared) continue; |
| final schemaUri = packageUri.resolve( |
| '../$package/doc/schema/${party.name}/' |
| 'shared_definitions.generated.schema.json', |
| ); |
| final contents = { |
| r'$schema': 'https://json-schema.org/draft-07/schema#', |
| r'$comment': |
| 'This schema was automatically generated. ' |
| 'It combines various definitions into new definitions.', |
| 'title': |
| 'package:$package party:${party.name} shared definitions generated', |
| 'definitions': { |
| for (final hook in Hook.values) |
| for (final inputOrOutput in InputOrOutput.values) ...{ |
| definitionName(hook, inputOrOutput): { |
| 'allOf': [ |
| // Include all definitions that together make this definition. |
| if (hook != Hook.hook) |
| { |
| r'$ref': |
| '#/definitions/${definitionName(Hook.hook, inputOrOutput)}', |
| }, |
| if (package != 'hooks') |
| { |
| r'$ref': |
| '../../../../hooks/doc/schema/${party.name}/shared_definitions${party == Party.shared ? '' : '.generated'}.schema.json#/definitions/${definitionName(hook, inputOrOutput)}', |
| }, |
| if (party != Party.shared) |
| { |
| r'$ref': |
| '../shared/shared_definitions${package == 'hooks' ? '' : '.generated'}.schema.json#/definitions/${definitionName(hook, inputOrOutput)}', |
| }, |
| if (package != 'data_assets' && party != Party.shared) |
| { |
| r'$ref': |
| 'shared_definitions.schema.json#/definitions/${definitionName(hook, inputOrOutput)}', |
| }, |
| // Redefine definitions if nested properties are overwritten. |
| if (party == Party.shared && |
| hook == Hook.build && |
| inputOrOutput == InputOrOutput.output) |
| buildOutputAssetOverride, |
| if (party == Party.shared && |
| hook == Hook.hook && |
| inputOrOutput == InputOrOutput.output) |
| hookOutputAssetOverride, |
| if (party == Party.shared && |
| hook == Hook.build && |
| inputOrOutput == InputOrOutput.input) |
| buildInputAssetOverride, |
| if (party == Party.shared && |
| hook == Hook.link && |
| inputOrOutput == InputOrOutput.input) |
| linkInputAssetOverride, |
| if (party == Party.shared && |
| hook == Hook.hook && |
| inputOrOutput == InputOrOutput.input && |
| package == 'code_assets') |
| hookInputCodeOverride, |
| ], |
| }, |
| if (package == 'code_assets' && |
| party == Party.shared && |
| hook == Hook.hook && |
| inputOrOutput == InputOrOutput.output) |
| ...configOverride, |
| }, |
| }, |
| }; |
| var jsonString = jsonEncoder.convert(sortJson(contents, schemaUri.path)); |
| jsonString += '\n'; |
| final file = File.fromUri(schemaUri); |
| |
| var oldContent = ''; |
| if (file.existsSync()) { |
| oldContent = file.readAsStringSync(); |
| } |
| |
| final newContentNormalized = jsonString.replaceAll('\r\n', '\n'); |
| final oldContentNormalized = oldContent.replaceAll('\r\n', '\n'); |
| if (newContentNormalized != oldContentNormalized) { |
| file.writeAsStringSync(jsonString); |
| print('Generated $schemaUri (content changed)'); |
| counts.changed++; |
| } |
| counts.generated++; |
| } |
| } |
| } |
| |
| void generateEntryPoints(Counts counts) { |
| for (final package in packages) { |
| for (final hook in Hook.values) { |
| if (hook == Hook.hook) continue; |
| for (final party in Party.values) { |
| if (party == Party.shared) continue; |
| for (final inputOrOutput in InputOrOutput.values) { |
| final schemaUri = packageUri.resolve( |
| '../$package/doc/schema/${party.name}/' |
| '${hook.name}_${inputOrOutput.name}.generated.schema.json', |
| ); |
| final name = definitionName(hook, inputOrOutput); |
| final contents = { |
| r'$schema': 'https://json-schema.org/draft-07/schema#', |
| r'$comment': |
| 'This schema was automatically generated. ' |
| 'It is the schema for a json file in the build/link hook protocol.', |
| 'title': 'package:$package party:${party.name} $name', |
| 'allOf': [ |
| { |
| r'$ref': |
| 'shared_definitions.generated.schema.json#/definitions/$name', |
| }, |
| ], |
| }; |
| |
| var jsonString = jsonEncoder.convert( |
| sortJson(contents, schemaUri.path), |
| ); |
| jsonString += '\n'; |
| final file = File.fromUri(schemaUri); |
| |
| var oldContent = ''; |
| if (file.existsSync()) { |
| oldContent = file.readAsStringSync(); |
| } |
| |
| final newContentNormalized = jsonString.replaceAll('\r\n', '\n'); |
| final oldContentNormalized = oldContent.replaceAll('\r\n', '\n'); |
| if (newContentNormalized != oldContentNormalized) { |
| file.writeAsStringSync(jsonString); |
| print('Generated $schemaUri (content changed)'); |
| counts.changed++; |
| } |
| counts.generated++; |
| } |
| } |
| } |
| } |
| } |
| |
| String definitionName(Hook hook, InputOrOutput inputOrOutput) => |
| '${ucFirst(hook.name)}${ucFirst(inputOrOutput.name)}'; |
| |
| String ucFirst(String str) { |
| if (str.isEmpty) { |
| return ''; |
| } |
| return str[0].toUpperCase() + str.substring(1); |
| } |