| // Copyright (c) 2026, 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. |
| |
| /// Tool for generating Exceptions to wrap JSON-RPC 2.0 error codes. |
| /// |
| /// In JSON-RPC 2.0 error messages carries: |
| /// * A numeric error code, |
| /// * A message, and, |
| /// * JSON data. |
| /// |
| /// The specification provides a short list of _standard error codes_ beyond |
| /// those applications have to define their own. For the communication between |
| /// `package:dartpad` the _Web Worker_ and the _sandboxed iframe_ we are using |
| /// JSON RPC 2.0, and the list of error codes is specified in: |
| /// |
| /// pkg/dartpad/lib/src/exceptions.yaml |
| /// |
| /// This script will read this YAML document and generate: |
| /// |
| /// pkg/dartpad/lib/src/exceptions.dart |
| /// |
| /// Containing an Exception class for each defined error code. |
| /// Furthermore, this script will update the documented table of errors in: |
| /// |
| /// pkg/dartpad/doc/worker-protocol.md |
| /// |
| /// Run this script after updating `exceptions.yaml` and take care that some |
| /// error codes (as noted in `exceptions.yaml`) must be manually sync'ed into |
| /// source files. |
| library; |
| |
| import 'dart:io'; |
| import 'dart:isolate'; |
| import 'package:yaml/yaml.dart'; |
| |
| void main() async { |
| // Resolve paths |
| final yamlUri = Isolate.resolvePackageUriSync( |
| Uri.parse('package:dartpad/src/exceptions.yaml'), |
| ); |
| final dartUri = Isolate.resolvePackageUriSync( |
| Uri.parse('package:dartpad/src/exceptions.dart'), |
| ); |
| |
| if (yamlUri == null || dartUri == null) { |
| print('Error: Could not resolve package:dartpad/src/exceptions.dart'); |
| exit(1); |
| } |
| |
| // package:dartpad/src/ resolves to lib/src/, so doc/ is at lib/../doc/ |
| final docUri = yamlUri.resolve('../../doc/worker-protocol.md'); |
| final docFile = File.fromUri(docUri); |
| final dartFile = File.fromUri(dartUri); |
| |
| // Parse YAML |
| final yamlString = File.fromUri(yamlUri).readAsStringSync(); |
| final yamlDoc = loadYaml(yamlString) as YamlMap; |
| |
| final groups = <ExceptionCodeGroup>[]; |
| for (final entry in yamlDoc['groups'] as YamlList) { |
| final g = entry as YamlMap; |
| final groupBaseName = g['name'] as String; |
| final description = g['description'] as String; |
| |
| final codes = <ExceptionCode>[]; |
| for (final entry in (g['codes'] as YamlMap).entries) { |
| final code = entry.key as int; |
| final details = entry.value as YamlMap; |
| |
| final baseName = details['name'] as String; |
| final className = '${baseName}Exception'; |
| final enumName = baseName[0].toLowerCase() + baseName.substring(1); |
| |
| codes.add( |
| ExceptionCode( |
| code: code, |
| className: className, |
| enumName: enumName, |
| description: (details['description'] as String).trim(), |
| ), |
| ); |
| } |
| // Sort by code to guarantee deterministic output |
| codes.sort((a, b) => a.code.compareTo(b.code)); |
| |
| groups.add( |
| ExceptionCodeGroup( |
| className: '${groupBaseName}Exception', |
| description: description, |
| codes: codes, |
| ), |
| ); |
| } |
| |
| // 3. Generate Dart File |
| _generateDartFile(dartFile, groups); |
| |
| // 4. Update Markdown File |
| _updateMarkdownFile(docFile, groups); |
| |
| print( |
| 'Successfully generated exceptions.dart and updated worker-protocol.md', |
| ); |
| } |
| |
| final class ExceptionCodeGroup { |
| final String className; |
| final String description; |
| final List<ExceptionCode> codes; |
| |
| ExceptionCodeGroup({ |
| required this.className, |
| required this.description, |
| required this.codes, |
| }); |
| } |
| |
| final class ExceptionCode { |
| final int code; |
| final String className; |
| final String enumName; |
| final String description; |
| |
| ExceptionCode({ |
| required this.code, |
| required this.className, |
| required this.enumName, |
| required this.description, |
| }); |
| } |
| |
| void _generateDartFile(File dartFile, List<ExceptionCodeGroup> groups) { |
| final buffer = StringBuffer(); |
| |
| final allCodes = groups.expand((g) => g.codes).toList(); |
| |
| buffer.writeln(''' |
| // Copyright (c) 2026, 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. |
| |
| // Generated code. Do not modify. |
| // |
| // This file is generated by tool/generate_exceptions.dart. |
| // To modify these exceptions, edit lib/src/exceptions.yaml and re-run. |
| |
| import 'package:json_rpc_2/json_rpc_2.dart'; |
| |
| /// JSON-RPC 2.0 error codes that may be returned by worker. |
| enum _ErrorCode {'''); |
| |
| for (final c in allCodes) { |
| buffer.writeln(' ${c.enumName}(${c.code}),'); |
| } |
| |
| buffer.writeln(''' |
| ; |
| |
| const _ErrorCode(this.code); |
| final int code; |
| } |
| |
| typedef _MakeException = Exception Function(String message, {Object? data}); |
| |
| /// Registry of all DartPadException constructors for use by |
| /// [rethrowAsDartPadException]. |
| final _exceptionRegistry = <_ErrorCode, _MakeException>{'''); |
| |
| for (final c in allCodes) { |
| buffer.writeln(' .${c.enumName}: ${c.className}.new,'); |
| } |
| |
| buffer.writeln(''' |
| }.map((key, value) => MapEntry(key.code, value)); |
| |
| /// Throw a [DartPadException] for [RpcException] depending on error code. |
| Never rethrowAsDartPadException(RpcException e) { |
| final f = _exceptionRegistry[e.code]; |
| if (f != null) { |
| throw f(e.message, data: e.data); |
| } |
| throw e; |
| } |
| |
| final class DartPadException extends RpcException { |
| DartPadException._(String message, _ErrorCode e, {super.data}) |
| : super(e.code, message); |
| |
| @override |
| String toString() => 'DartPadException(\$code: \$message)'; |
| } |
| '''); |
| |
| for (final g in groups) { |
| buffer.writeln(''' |
| ${wrapDescription(g.description)} |
| abstract final class ${g.className} extends DartPadException { |
| ${g.className}._(super.message, super.e, {super.data}) : super._(); |
| |
| @override String toString() => |
| '${g.className}(\$code: \$message)'; |
| } |
| '''); |
| |
| for (final c in g.codes) { |
| buffer.writeln(''' |
| ${wrapDescription(c.description)} |
| final class ${c.className} extends ${g.className} { |
| ${c.className}(String message, {super.data}) |
| : super._(message, .${c.enumName}); |
| |
| @override String toString() => |
| '${c.className}(\$code: \$message)'; |
| } |
| '''); |
| } |
| } |
| |
| dartFile.writeAsStringSync(buffer.toString()); |
| |
| // Run `dart format` to ensure the generated file looks perfect |
| Process.runSync(Platform.resolvedExecutable, ['format', dartFile.path]); |
| } |
| |
| void _updateMarkdownFile(File docFile, List<ExceptionCodeGroup> groups) { |
| if (!docFile.existsSync()) { |
| print('Warning: Markdown file not found at ${docFile.path}'); |
| return; |
| } |
| final allCodes = groups.expand((g) => g.codes).toList(); |
| |
| final content = docFile.readAsStringSync(); |
| final beginMarker = '<!-- BEGIN GENERATED ERROR CODE TABLE -->'; |
| final endMarker = '<!-- END GENERATED ERROR CODE TABLE -->'; |
| |
| final regex = RegExp( |
| '${RegExp.escape(beginMarker)}(.*?)${RegExp.escape(endMarker)}', |
| dotAll: true, |
| ); |
| |
| if (!regex.hasMatch(content)) { |
| print( |
| 'Warning: Could not find generated table markers in worker-protocol.md', |
| ); |
| return; |
| } |
| |
| final tableBuffer = StringBuffer(); |
| tableBuffer.writeln('\n| Code | Name | Description |'); |
| tableBuffer.writeln('| :--- | :--- | :--- |'); |
| |
| for (final c in allCodes) { |
| // Replace newlines in description with spaces so it doesn't break the |
| // markdown table |
| final safeDescription = c.description.replaceAll('\n', ' '); |
| tableBuffer.writeln('| ${c.code} | `${c.enumName}` | $safeDescription |'); |
| } |
| tableBuffer.writeln(); |
| |
| final newContent = content.replaceFirst( |
| regex, |
| '$beginMarker${tableBuffer.toString()}$endMarker', |
| ); |
| |
| docFile.writeAsStringSync(newContent); |
| } |
| |
| String wrapDescription(String text, {int width = 80, String prefix = '/// '}) { |
| final lines = text.split('\n'); |
| final resultBuffer = StringBuffer(); |
| |
| for (var i = 0; i < lines.length; i++) { |
| final line = lines[i]; |
| |
| if (line.trim().isEmpty) { |
| // 2. Preserve empty lines (just the prefix) |
| resultBuffer.write(prefix.trimRight()); |
| } else { |
| // 3. Wrap the content of the current line |
| final words = line.split(' '); |
| var currentLine = prefix; |
| |
| for (final word in words) { |
| if ((currentLine + word).length > width) { |
| resultBuffer.writeln(currentLine.trimRight()); |
| currentLine = '$prefix$word '; |
| } else { |
| currentLine += '$word '; |
| } |
| } |
| resultBuffer.write(currentLine.trimRight()); |
| } |
| |
| if (i < lines.length - 1) { |
| resultBuffer.writeln(); |
| } |
| } |
| |
| return resultBuffer.toString(); |
| } |