blob: 6ce7616ec6e584296e1892d6c27a08636ef7cb91 [file] [edit]
// 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 sandboxJsUri = yamlUri.resolve(
'../../../dartpad_worker/lib/src/asset/sandbox.js',
);
final docFile = File.fromUri(docUri);
final dartFile = File.fromUri(dartUri);
final sandboxJsFile = File.fromUri(sandboxJsUri);
// 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);
print(' - updated exceptions.dart');
// 4. Update Markdown File
_updateMarkdownFile(docFile, groups);
print(' - updated worker-protocol.md');
// 5. Update sandbox.js
_updateSandboxJs(sandboxJsFile, groups);
print(' - updated sandbox.js');
print('Successfully generated');
}
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('Error: Markdown file not found at ${docFile.path}');
exit(1);
}
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(
'Error: Could not find generated table markers in worker-protocol.md',
);
exit(1);
}
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();
}
void _updateSandboxJs(File jsFile, List<ExceptionCodeGroup> groups) {
if (!jsFile.existsSync()) {
print('Error: JS file not found at ${jsFile.path}');
exit(1);
}
final sandboxGroup = groups
.where((g) => g.className.toLowerCase().contains('sandbox'))
.firstOrNull;
if (sandboxGroup == null) {
print('Error: Sandbox group not found in exceptions.yaml');
exit(1);
}
final lines = jsFile.readAsLinesSync();
final beginMarker = '/* BEGIN GENERATED ERROR CODE TABLE */'
.replaceAll(' ', r'\s+')
.replaceAll('/*', r'/[\*\s]+')
.replaceAll('*/', r'[\*\s]+/');
final beginRegex = RegExp('^(\\s*)$beginMarker\$');
final endMarker = '/* END GENERATED ERROR CODE TABLE */'
.replaceAll(' ', r'\s+')
.replaceAll('/*', r'/[\*\s]+')
.replaceAll('*/', r'[\*\s]+/');
final endRegex = RegExp('^\\s*$endMarker\$');
final newLines = <String>[];
var insideTable = false;
var foundTable = false;
for (final line in lines) {
if (!insideTable) {
newLines.add(line);
final match = beginRegex.firstMatch(line);
if (match != null) {
insideTable = true;
foundTable = true;
final indent = match.group(1)!;
for (final c in sandboxGroup.codes) {
final constantName = _toConstantName(c.enumName);
newLines.add('$indent$constantName: ${c.code},');
}
}
} else if (endRegex.hasMatch(line)) {
insideTable = false;
newLines.add(line);
}
}
if (!foundTable) {
print('Error: Could not find generated table markers in sandbox.js');
exit(1);
}
if (insideTable) {
print('Error: Could not find end table marker in sandbox.js');
exit(1);
}
jsFile.writeAsStringSync('${newLines.join('\n')}\n');
}
String _toConstantName(String enumName) {
return enumName.replaceAllMapped(RegExp(r'[A-Z]'), (match) {
return '_${match.group(0)!}';
}).toUpperCase();
}