render-workflows-dart 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/LICENSE +21 -0
- package/README.md +678 -0
- package/dart/generator/bin/generate.dart +419 -0
- package/dart/generator/pubspec.lock +149 -0
- package/dart/generator/pubspec.yaml +9 -0
- package/examples/README.md +56 -0
- package/examples/default/README.md +18 -0
- package/examples/default/gitignore +7 -0
- package/examples/default/index.js +2 -0
- package/examples/default/package.json +17 -0
- package/examples/default/pubspec.yaml +7 -0
- package/examples/default/tasks.dart +39 -0
- package/examples/http/README.md +26 -0
- package/examples/http/gitignore +7 -0
- package/examples/http/index.js +2 -0
- package/examples/http/package.json +18 -0
- package/examples/http/pubspec.yaml +13 -0
- package/examples/http/tasks.dart +100 -0
- package/examples/introspect/README.md +50 -0
- package/examples/introspect/gitignore +7 -0
- package/examples/introspect/index.js +2 -0
- package/examples/introspect/node_env.dart +35 -0
- package/examples/introspect/package.json +18 -0
- package/examples/introspect/pubspec.yaml +23 -0
- package/examples/introspect/tasks.dart +144 -0
- package/examples/native/README.md +32 -0
- package/examples/native/gitignore +7 -0
- package/examples/native/index.js +2 -0
- package/examples/native/native/tools_impl.dart +105 -0
- package/examples/native/package.json +23 -0
- package/examples/native/pubspec.yaml +7 -0
- package/examples/native/tasks.dart +35 -0
- package/examples/postgres/README.md +73 -0
- package/examples/postgres/gitignore +7 -0
- package/examples/postgres/index.js +2 -0
- package/examples/postgres/native/db_impl.dart +205 -0
- package/examples/postgres/package.json +26 -0
- package/examples/postgres/pubspec.yaml +14 -0
- package/examples/postgres/seed/bin/seed.dart +56 -0
- package/examples/postgres/seed/bin/show.dart +64 -0
- package/examples/postgres/seed/lib/src/connect.dart +93 -0
- package/examples/postgres/seed/lib/src/schema.dart +46 -0
- package/examples/postgres/seed/pubspec.yaml +17 -0
- package/examples/postgres/tasks.dart +66 -0
- package/package.json +51 -0
- package/runtime/AGENTS.md +141 -0
- package/runtime/CLAUDE.md +2 -0
- package/runtime/native_task.dart +115 -0
- package/runtime/render_dart.dart +428 -0
- package/src/cli.js +396 -0
- package/src/native-worker.js +196 -0
- package/src/node-bridge.js +118 -0
- package/src/runtime.js +108 -0
- package/src/toolchain/compile.js +153 -0
- package/src/toolchain/dart-sdk.js +216 -0
- package/src/toolchain/dart-version.js +113 -0
- package/src/toolchain/generate.js +112 -0
- package/src/toolchain/index.js +8 -0
- package/src/toolchain/native.js +217 -0
- package/src/web-shims.js +281 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// Generates the two halves of a native task from an annotated Dart file.
|
|
2
|
+
//
|
|
3
|
+
// Reads a file containing `@nativeTask` top-level functions and writes:
|
|
4
|
+
//
|
|
5
|
+
// <name>.g.dart typed stubs, imported by the dart2js task code
|
|
6
|
+
// <name>.main.dart the dispatcher main(), which is what compiles AOT
|
|
7
|
+
//
|
|
8
|
+
// The analyzer is used rather than pattern matching because the stubs have to
|
|
9
|
+
// reproduce parameter names, defaults, nullability and generics exactly — a
|
|
10
|
+
// regex that is right most of the time would produce code that fails to
|
|
11
|
+
// compile in ways the author cannot fix.
|
|
12
|
+
import 'dart:convert';
|
|
13
|
+
import 'dart:io';
|
|
14
|
+
|
|
15
|
+
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
|
|
16
|
+
import 'package:analyzer/dart/analysis/results.dart';
|
|
17
|
+
import 'package:analyzer/dart/element/element.dart';
|
|
18
|
+
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
|
19
|
+
import 'package:analyzer/dart/element/type.dart';
|
|
20
|
+
|
|
21
|
+
Future<int> main(List<String> argv) async {
|
|
22
|
+
final args = _parseArgs(argv);
|
|
23
|
+
final project = args['project']!;
|
|
24
|
+
final entry = args['entry']!;
|
|
25
|
+
final name = args['name']!;
|
|
26
|
+
final stubPath = args['stub']!;
|
|
27
|
+
final mainPath = args['main']!;
|
|
28
|
+
final facadePath = args['facade']!;
|
|
29
|
+
// package.json overrides the annotation; absent means "annotation decides".
|
|
30
|
+
final override = _Options(
|
|
31
|
+
worker: args['worker'] == null ? null : args['worker'] == 'true',
|
|
32
|
+
idleTimeoutMs: int.tryParse(args['idle'] ?? ''),
|
|
33
|
+
timeoutMs: int.tryParse(args['timeout'] ?? ''),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
final collection = AnalysisContextCollection(includedPaths: [project, entry]);
|
|
37
|
+
final session = collection.contextFor(entry).currentSession;
|
|
38
|
+
final resolved = await session.getResolvedLibrary(entry);
|
|
39
|
+
|
|
40
|
+
if (resolved is! ResolvedLibraryResult) {
|
|
41
|
+
return _fail('could not analyse $entry: $resolved');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
final errors = <String>[];
|
|
45
|
+
for (final unit in resolved.units) {
|
|
46
|
+
for (final d in unit.diagnostics) {
|
|
47
|
+
if (d.severity.name == 'ERROR') {
|
|
48
|
+
errors.add(' ${d.message} (line ${unit.lineInfo.getLocation(d.offset).lineNumber})');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (errors.isNotEmpty) {
|
|
53
|
+
return _fail('${_rel(project, entry)} does not analyse cleanly:\n${errors.join('\n')}');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
final fns = resolved.element.topLevelFunctions.where(_isNativeTask).toList();
|
|
57
|
+
if (fns.isEmpty) {
|
|
58
|
+
return _fail(
|
|
59
|
+
'no @nativeTask functions in ${_rel(project, entry)}.\n'
|
|
60
|
+
'Annotate a top-level function with @nativeTask, or declare this entry '
|
|
61
|
+
'with "mode": "exe" if it owns its own main().',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
final problems = <String>[];
|
|
66
|
+
for (final fn in fns) {
|
|
67
|
+
problems.addAll(_validate(fn));
|
|
68
|
+
}
|
|
69
|
+
if (problems.isNotEmpty) {
|
|
70
|
+
return _fail(
|
|
71
|
+
'these @nativeTask signatures cannot cross a JSON boundary:\n'
|
|
72
|
+
'${problems.map((p) => ' $p').join('\n')}\n\n'
|
|
73
|
+
'Supported: bool, int, double, num, String, List<T>, Map<String, T>, '
|
|
74
|
+
'Object?, dynamic, and Future<T> of those.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
File(stubPath)
|
|
79
|
+
..createSync(recursive: true)
|
|
80
|
+
..writeAsStringSync(_stubs(name, fns, stubPath, project, override));
|
|
81
|
+
File(facadePath)
|
|
82
|
+
..createSync(recursive: true)
|
|
83
|
+
..writeAsStringSync(_facade(name, stubPath, facadePath, entry));
|
|
84
|
+
File(mainPath)
|
|
85
|
+
..createSync(recursive: true)
|
|
86
|
+
..writeAsStringSync(_dispatcher(name, fns, mainPath, entry, project));
|
|
87
|
+
|
|
88
|
+
stdout.writeln(jsonEncode({
|
|
89
|
+
'name': name,
|
|
90
|
+
'methods': [for (final f in fns) f.name],
|
|
91
|
+
}));
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------- annotations
|
|
96
|
+
|
|
97
|
+
/// Effective settings for one native task.
|
|
98
|
+
class _Options {
|
|
99
|
+
const _Options({this.worker, this.idleTimeoutMs, this.timeoutMs});
|
|
100
|
+
|
|
101
|
+
final bool? worker;
|
|
102
|
+
final int? idleTimeoutMs;
|
|
103
|
+
final int? timeoutMs;
|
|
104
|
+
|
|
105
|
+
/// [other] wins where it has an opinion. Used for package.json over the
|
|
106
|
+
/// annotation, so a deployment can change behaviour without editing code.
|
|
107
|
+
_Options overriddenBy(_Options other) => _Options(
|
|
108
|
+
worker: other.worker ?? worker,
|
|
109
|
+
idleTimeoutMs: other.idleTimeoutMs ?? idleTimeoutMs,
|
|
110
|
+
timeoutMs: other.timeoutMs ?? timeoutMs,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Reads `@NativeTask(...)` arguments off the function.
|
|
115
|
+
_Options _optionsOf(TopLevelFunctionElement fn) {
|
|
116
|
+
for (final a in fn.metadata.annotations) {
|
|
117
|
+
final value = a.computeConstantValue();
|
|
118
|
+
if (value?.type?.element?.name != 'NativeTask') continue;
|
|
119
|
+
|
|
120
|
+
// A const Duration exposes its microseconds as a field on the constant.
|
|
121
|
+
// Current SDKs name it `inMicroseconds`; older ones used the private
|
|
122
|
+
// `_duration`, so both are tried rather than silently reading null and
|
|
123
|
+
// falling back to a default the author did not ask for.
|
|
124
|
+
int? ms(String field) {
|
|
125
|
+
final duration = value!.getField(field);
|
|
126
|
+
if (duration == null || duration.isNull) return null;
|
|
127
|
+
|
|
128
|
+
final micros = duration.getField('inMicroseconds')?.toIntValue() ??
|
|
129
|
+
duration.getField('_duration')?.toIntValue();
|
|
130
|
+
if (micros == null) {
|
|
131
|
+
stderr.writeln(
|
|
132
|
+
'warning: could not read $field from @NativeTask on ${fn.name}; '
|
|
133
|
+
'using the default instead',
|
|
134
|
+
);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return micros ~/ 1000;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return _Options(
|
|
141
|
+
worker: value!.getField('worker')?.toBoolValue(),
|
|
142
|
+
idleTimeoutMs: ms('idleTimeout'),
|
|
143
|
+
timeoutMs: ms('timeout'),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return const _Options();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bool _isNativeTask(TopLevelFunctionElement fn) {
|
|
150
|
+
for (final a in fn.metadata.annotations) {
|
|
151
|
+
// Matches both `@nativeTask` (a const instance) and `@NativeTask()`.
|
|
152
|
+
final value = a.computeConstantValue();
|
|
153
|
+
final typeName = value?.type?.element?.name;
|
|
154
|
+
if (typeName == 'NativeTask') return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ----------------------------------------------------------------- validation
|
|
160
|
+
|
|
161
|
+
/// Unwraps `Future<T>` to `T`; other types are returned unchanged.
|
|
162
|
+
DartType _unwrapFuture(DartType t) {
|
|
163
|
+
if (t is InterfaceType && t.isDartAsyncFuture && t.typeArguments.length == 1) {
|
|
164
|
+
return t.typeArguments.single;
|
|
165
|
+
}
|
|
166
|
+
return t;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
bool _jsonable(DartType t) {
|
|
170
|
+
if (t is VoidType || t is DynamicType) return true;
|
|
171
|
+
if (t.isDartCoreBool || t.isDartCoreInt || t.isDartCoreDouble) return true;
|
|
172
|
+
if (t.isDartCoreNum || t.isDartCoreString || t.isDartCoreNull) return true;
|
|
173
|
+
if (t.isDartCoreObject) return true;
|
|
174
|
+
if (t is InterfaceType) {
|
|
175
|
+
if (t.isDartCoreList) return _jsonable(t.typeArguments.single);
|
|
176
|
+
if (t.isDartCoreMap) {
|
|
177
|
+
return t.typeArguments[0].isDartCoreString && _jsonable(t.typeArguments[1]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
List<String> _validate(TopLevelFunctionElement fn) {
|
|
184
|
+
final out = <String>[];
|
|
185
|
+
final ret = _unwrapFuture(fn.returnType);
|
|
186
|
+
if (!_jsonable(ret)) {
|
|
187
|
+
out.add('${fn.name}: return type ${_display(fn.returnType)}');
|
|
188
|
+
}
|
|
189
|
+
for (final p in fn.formalParameters) {
|
|
190
|
+
if (!_jsonable(p.type)) {
|
|
191
|
+
out.add('${fn.name}: parameter "${p.name}" of type ${_display(p.type)}');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// -------------------------------------------------------------------- codegen
|
|
198
|
+
|
|
199
|
+
String _display(DartType t) => t.getDisplayString();
|
|
200
|
+
|
|
201
|
+
bool _nullable(DartType t) =>
|
|
202
|
+
t.nullabilitySuffix == NullabilitySuffix.question || t is DynamicType;
|
|
203
|
+
|
|
204
|
+
/// A Dart expression converting `expr` (raw decoded JSON) into [t].
|
|
205
|
+
///
|
|
206
|
+
/// A plain `as` cast is not enough: jsonDecode produces `List<dynamic>` and
|
|
207
|
+
/// `Map<String, dynamic>`, so `as List<int>` throws even when every element is
|
|
208
|
+
/// an int. Collections are rebuilt element by element, and doubles go through
|
|
209
|
+
/// num because JSON does not distinguish 3 from 3.0.
|
|
210
|
+
String _decode(DartType t, String expr) {
|
|
211
|
+
if (t is VoidType || t is DynamicType || t.isDartCoreObject) return expr;
|
|
212
|
+
|
|
213
|
+
final q = _nullable(t);
|
|
214
|
+
final bang = q ? '?' : '';
|
|
215
|
+
|
|
216
|
+
if (t.isDartCoreDouble) {
|
|
217
|
+
return q ? '($expr as num?)?.toDouble()' : '($expr as num).toDouble()';
|
|
218
|
+
}
|
|
219
|
+
if (t.isDartCoreBool || t.isDartCoreInt || t.isDartCoreNum || t.isDartCoreString) {
|
|
220
|
+
return '$expr as ${_display(t)}';
|
|
221
|
+
}
|
|
222
|
+
if (t is InterfaceType && t.isDartCoreList) {
|
|
223
|
+
final e = t.typeArguments.single;
|
|
224
|
+
return '($expr as List$bang)$bang.map((e) => ${_decode(e, 'e')}).toList()';
|
|
225
|
+
}
|
|
226
|
+
if (t is InterfaceType && t.isDartCoreMap) {
|
|
227
|
+
final v = t.typeArguments[1];
|
|
228
|
+
return '($expr as Map$bang)$bang.map((k, e) => '
|
|
229
|
+
'MapEntry(k as String, ${_decode(v, 'e')}))';
|
|
230
|
+
}
|
|
231
|
+
return expr;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
String _rel(String from, String to) {
|
|
235
|
+
final f = Directory(from).absolute.path.split(Platform.pathSeparator);
|
|
236
|
+
final t = File(to).absolute.path.split(Platform.pathSeparator);
|
|
237
|
+
var i = 0;
|
|
238
|
+
while (i < f.length && i < t.length && f[i] == t[i]) i++;
|
|
239
|
+
return [...List.filled(f.length - i, '..'), ...t.sublist(i)].join('/');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Import path from the file at [fromFile] to [toFile].
|
|
243
|
+
String _importPath(String fromFile, String toFile) =>
|
|
244
|
+
_rel(File(fromFile).parent.path, toFile);
|
|
245
|
+
|
|
246
|
+
String _header(String name) => '''
|
|
247
|
+
// GENERATED by render-dart from a @nativeTask source. Do not edit.
|
|
248
|
+
//
|
|
249
|
+
// Regenerated by `render-dart build`; changes here are overwritten.
|
|
250
|
+
''';
|
|
251
|
+
|
|
252
|
+
String _stubs(
|
|
253
|
+
String name,
|
|
254
|
+
List<TopLevelFunctionElement> fns,
|
|
255
|
+
String stubPath,
|
|
256
|
+
String project,
|
|
257
|
+
_Options override,
|
|
258
|
+
) {
|
|
259
|
+
final b = StringBuffer(_header(name))
|
|
260
|
+
..writeln("import '${_importPath(stubPath, '$project/render_dart.dart')}';")
|
|
261
|
+
..writeln();
|
|
262
|
+
|
|
263
|
+
for (final fn in fns) {
|
|
264
|
+
final ret = _unwrapFuture(fn.returnType);
|
|
265
|
+
final isVoid = ret is VoidType;
|
|
266
|
+
final sig = _stubSignature(fn);
|
|
267
|
+
final positional = fn.formalParameters.where((p) => !p.isNamed).toList();
|
|
268
|
+
final named = fn.formalParameters.where((p) => p.isNamed).toList();
|
|
269
|
+
|
|
270
|
+
final args = '[${positional.map((p) => p.name).join(', ')}]';
|
|
271
|
+
final namedMap = named.isEmpty
|
|
272
|
+
? 'const {}'
|
|
273
|
+
: '{${named.map((p) => "'${p.name}': ${p.name}").join(', ')}}';
|
|
274
|
+
|
|
275
|
+
b
|
|
276
|
+
..writeln('/// Runs `${fn.name}` in the `$name` native executable.')
|
|
277
|
+
..writeln('Future<${isVoid ? 'void' : _display(ret)}> $sig async {');
|
|
278
|
+
// Settings ride with the declaration, so nothing at the call site has to
|
|
279
|
+
// know this is native.
|
|
280
|
+
final o = _optionsOf(fn).overriddenBy(override);
|
|
281
|
+
final tail = (o.worker ?? false)
|
|
282
|
+
? ', true, ${o.idleTimeoutMs ?? 30000}, ${o.timeoutMs ?? 0}'
|
|
283
|
+
: (o.timeoutMs != null ? ', false, 30000, ${o.timeoutMs}' : '');
|
|
284
|
+
final call = "callNativeTask('$name', '${fn.name}', $args, $namedMap$tail)";
|
|
285
|
+
if (isVoid) {
|
|
286
|
+
b.writeln(' await $call;');
|
|
287
|
+
} else {
|
|
288
|
+
b
|
|
289
|
+
..writeln(' final r = await $call;')
|
|
290
|
+
..writeln(' return ${_decode(ret, 'r')};');
|
|
291
|
+
}
|
|
292
|
+
b
|
|
293
|
+
..writeln('}')
|
|
294
|
+
..writeln();
|
|
295
|
+
}
|
|
296
|
+
return b.toString();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
String _stubSignature(TopLevelFunctionElement fn) {
|
|
300
|
+
final positional = <String>[];
|
|
301
|
+
final optional = <String>[];
|
|
302
|
+
final named = <String>[];
|
|
303
|
+
|
|
304
|
+
for (final p in fn.formalParameters) {
|
|
305
|
+
final decl = '${_display(p.type)} ${p.name}';
|
|
306
|
+
if (p.isNamed) {
|
|
307
|
+
final def = p.hasDefaultValue ? ' = ${p.defaultValueCode}' : '';
|
|
308
|
+
named.add(p.isRequiredNamed ? 'required $decl' : '$decl$def');
|
|
309
|
+
} else if (p.isOptionalPositional) {
|
|
310
|
+
optional.add(p.hasDefaultValue ? '$decl = ${p.defaultValueCode}' : decl);
|
|
311
|
+
} else {
|
|
312
|
+
positional.add(decl);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
final parts = [
|
|
317
|
+
...positional,
|
|
318
|
+
if (optional.isNotEmpty) '[${optional.join(', ')}]',
|
|
319
|
+
if (named.isNotEmpty) '{${named.join(', ')}}',
|
|
320
|
+
];
|
|
321
|
+
return '${fn.name}(${parts.join(', ')})';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// The file callers import.
|
|
325
|
+
///
|
|
326
|
+
/// A conditional export, so the same import resolves to the real
|
|
327
|
+
/// implementation when compiled AOT and to the process-spawning stub under
|
|
328
|
+
/// dart2js. That is what lets task code call a native task by its plain name
|
|
329
|
+
/// with no knowledge that it is native — and lets native code calling a
|
|
330
|
+
/// sibling native function skip the process hop entirely.
|
|
331
|
+
String _facade(String name, String stubPath, String facadePath, String entry) => '''
|
|
332
|
+
${_header(name)}// Callers import this file, not the implementation beside it.
|
|
333
|
+
//
|
|
334
|
+
// Under dart2js `dart.library.io` is false, so this resolves to the stub that
|
|
335
|
+
// spawns the executable. Compiled AOT it resolves to the implementation and the
|
|
336
|
+
// call is direct.
|
|
337
|
+
//
|
|
338
|
+
// Always `await` a native task: the stub returns a Future where the
|
|
339
|
+
// implementation may return a plain value, so awaiting is what makes one piece
|
|
340
|
+
// of code compile against both.
|
|
341
|
+
export '${_importPath(facadePath, stubPath)}'
|
|
342
|
+
if (dart.library.io) '${_importPath(facadePath, entry)}';
|
|
343
|
+
''';
|
|
344
|
+
|
|
345
|
+
String _dispatcher(
|
|
346
|
+
String name,
|
|
347
|
+
List<TopLevelFunctionElement> fns,
|
|
348
|
+
String mainPath,
|
|
349
|
+
String entry,
|
|
350
|
+
String project,
|
|
351
|
+
) {
|
|
352
|
+
final b = StringBuffer(_header(name))
|
|
353
|
+
..writeln("import '${_importPath(mainPath, entry)}';")
|
|
354
|
+
..writeln("import '${_importPath(mainPath, '$project/native_task.dart')}';")
|
|
355
|
+
..writeln()
|
|
356
|
+
..writeln('Future<void> main(List<String> args) => nativeTaskMain(args, {');
|
|
357
|
+
|
|
358
|
+
for (final fn in fns) {
|
|
359
|
+
final call = <String>[];
|
|
360
|
+
var i = 0;
|
|
361
|
+
for (final p in fn.formalParameters) {
|
|
362
|
+
if (p.isNamed) {
|
|
363
|
+
final fallback = p.hasDefaultValue
|
|
364
|
+
? p.defaultValueCode!
|
|
365
|
+
: (_nullable(p.type) ? 'null' : "_missing('${fn.name}', '${p.name}')");
|
|
366
|
+
call.add(
|
|
367
|
+
"${p.name}: n.containsKey('${p.name}') "
|
|
368
|
+
'? ${_decode(p.type, "n['${p.name}']")} : $fallback',
|
|
369
|
+
);
|
|
370
|
+
} else {
|
|
371
|
+
final idx = i++;
|
|
372
|
+
final read = _decode(p.type, 'a[$idx]');
|
|
373
|
+
if (p.isOptionalPositional) {
|
|
374
|
+
final fallback = p.hasDefaultValue
|
|
375
|
+
? p.defaultValueCode!
|
|
376
|
+
: (_nullable(p.type) ? 'null' : "_missing('${fn.name}', '${p.name}')");
|
|
377
|
+
call.add('a.length > $idx ? $read : $fallback');
|
|
378
|
+
} else {
|
|
379
|
+
call.add(read);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
b.writeln(" '${fn.name}': (a, n) async => ${fn.name}(${call.join(', ')}),");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
b.writeln('});');
|
|
387
|
+
|
|
388
|
+
final body = b.toString();
|
|
389
|
+
if (!body.contains('_missing(')) return body;
|
|
390
|
+
|
|
391
|
+
return '$body\n'
|
|
392
|
+
'Never _missing(String fn, String param) =>\n'
|
|
393
|
+
" throw ArgumentError('\$fn requires the parameter \"\$param\"');\n";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ------------------------------------------------------------------- plumbing
|
|
397
|
+
|
|
398
|
+
Map<String, String> _parseArgs(List<String> argv) {
|
|
399
|
+
final out = <String, String>{};
|
|
400
|
+
for (var i = 0; i < argv.length; i++) {
|
|
401
|
+
if (argv[i].startsWith('--')) out[argv[i].substring(2)] = argv[++i];
|
|
402
|
+
}
|
|
403
|
+
for (final k in ['project', 'entry', 'name', 'stub', 'main', 'facade']) {
|
|
404
|
+
if (!out.containsKey(k)) {
|
|
405
|
+
stderr.writeln('generate.dart: missing --$k');
|
|
406
|
+
exit(2);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
int _fail(String message) {
|
|
413
|
+
stderr.writeln(message);
|
|
414
|
+
// An async main's return value is NOT the process exit code — only a
|
|
415
|
+
// synchronous `int main()` works that way. Setting exitCode is what actually
|
|
416
|
+
// makes the build fail instead of silently continuing with no output.
|
|
417
|
+
exitCode = 1;
|
|
418
|
+
return 1;
|
|
419
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Generated by pub
|
|
2
|
+
# See https://dart.dev/tools/pub/glossary#lockfile
|
|
3
|
+
packages:
|
|
4
|
+
_fe_analyzer_shared:
|
|
5
|
+
dependency: transitive
|
|
6
|
+
description:
|
|
7
|
+
name: _fe_analyzer_shared
|
|
8
|
+
sha256: "9a3386eea899815698dd55995277cf7cb8572ee52b399a6edfb7ae2b50e5fc19"
|
|
9
|
+
url: "https://pub.dev"
|
|
10
|
+
source: hosted
|
|
11
|
+
version: "105.0.0"
|
|
12
|
+
analyzer:
|
|
13
|
+
dependency: "direct main"
|
|
14
|
+
description:
|
|
15
|
+
name: analyzer
|
|
16
|
+
sha256: "62993bed6eadbe9596c5c20d5c167e7bc563c5fe266657a04ddeb93bdb84f4c9"
|
|
17
|
+
url: "https://pub.dev"
|
|
18
|
+
source: hosted
|
|
19
|
+
version: "14.1.0"
|
|
20
|
+
async:
|
|
21
|
+
dependency: transitive
|
|
22
|
+
description:
|
|
23
|
+
name: async
|
|
24
|
+
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
|
25
|
+
url: "https://pub.dev"
|
|
26
|
+
source: hosted
|
|
27
|
+
version: "2.13.1"
|
|
28
|
+
collection:
|
|
29
|
+
dependency: transitive
|
|
30
|
+
description:
|
|
31
|
+
name: collection
|
|
32
|
+
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
|
33
|
+
url: "https://pub.dev"
|
|
34
|
+
source: hosted
|
|
35
|
+
version: "1.19.1"
|
|
36
|
+
convert:
|
|
37
|
+
dependency: transitive
|
|
38
|
+
description:
|
|
39
|
+
name: convert
|
|
40
|
+
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
|
41
|
+
url: "https://pub.dev"
|
|
42
|
+
source: hosted
|
|
43
|
+
version: "3.1.2"
|
|
44
|
+
crypto:
|
|
45
|
+
dependency: transitive
|
|
46
|
+
description:
|
|
47
|
+
name: crypto
|
|
48
|
+
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
|
49
|
+
url: "https://pub.dev"
|
|
50
|
+
source: hosted
|
|
51
|
+
version: "3.0.7"
|
|
52
|
+
file:
|
|
53
|
+
dependency: transitive
|
|
54
|
+
description:
|
|
55
|
+
name: file
|
|
56
|
+
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
|
57
|
+
url: "https://pub.dev"
|
|
58
|
+
source: hosted
|
|
59
|
+
version: "7.0.1"
|
|
60
|
+
glob:
|
|
61
|
+
dependency: transitive
|
|
62
|
+
description:
|
|
63
|
+
name: glob
|
|
64
|
+
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
|
65
|
+
url: "https://pub.dev"
|
|
66
|
+
source: hosted
|
|
67
|
+
version: "2.1.3"
|
|
68
|
+
meta:
|
|
69
|
+
dependency: transitive
|
|
70
|
+
description:
|
|
71
|
+
name: meta
|
|
72
|
+
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
|
73
|
+
url: "https://pub.dev"
|
|
74
|
+
source: hosted
|
|
75
|
+
version: "1.19.0"
|
|
76
|
+
package_config:
|
|
77
|
+
dependency: transitive
|
|
78
|
+
description:
|
|
79
|
+
name: package_config
|
|
80
|
+
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
|
|
81
|
+
url: "https://pub.dev"
|
|
82
|
+
source: hosted
|
|
83
|
+
version: "3.0.0"
|
|
84
|
+
path:
|
|
85
|
+
dependency: transitive
|
|
86
|
+
description:
|
|
87
|
+
name: path
|
|
88
|
+
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
|
89
|
+
url: "https://pub.dev"
|
|
90
|
+
source: hosted
|
|
91
|
+
version: "1.9.1"
|
|
92
|
+
pub_semver:
|
|
93
|
+
dependency: transitive
|
|
94
|
+
description:
|
|
95
|
+
name: pub_semver
|
|
96
|
+
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
|
97
|
+
url: "https://pub.dev"
|
|
98
|
+
source: hosted
|
|
99
|
+
version: "2.2.0"
|
|
100
|
+
source_span:
|
|
101
|
+
dependency: transitive
|
|
102
|
+
description:
|
|
103
|
+
name: source_span
|
|
104
|
+
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
|
105
|
+
url: "https://pub.dev"
|
|
106
|
+
source: hosted
|
|
107
|
+
version: "1.10.2"
|
|
108
|
+
string_scanner:
|
|
109
|
+
dependency: transitive
|
|
110
|
+
description:
|
|
111
|
+
name: string_scanner
|
|
112
|
+
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
|
113
|
+
url: "https://pub.dev"
|
|
114
|
+
source: hosted
|
|
115
|
+
version: "1.4.1"
|
|
116
|
+
term_glyph:
|
|
117
|
+
dependency: transitive
|
|
118
|
+
description:
|
|
119
|
+
name: term_glyph
|
|
120
|
+
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
|
121
|
+
url: "https://pub.dev"
|
|
122
|
+
source: hosted
|
|
123
|
+
version: "1.2.2"
|
|
124
|
+
typed_data:
|
|
125
|
+
dependency: transitive
|
|
126
|
+
description:
|
|
127
|
+
name: typed_data
|
|
128
|
+
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
|
129
|
+
url: "https://pub.dev"
|
|
130
|
+
source: hosted
|
|
131
|
+
version: "1.4.0"
|
|
132
|
+
watcher:
|
|
133
|
+
dependency: transitive
|
|
134
|
+
description:
|
|
135
|
+
name: watcher
|
|
136
|
+
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
|
137
|
+
url: "https://pub.dev"
|
|
138
|
+
source: hosted
|
|
139
|
+
version: "1.2.1"
|
|
140
|
+
yaml:
|
|
141
|
+
dependency: transitive
|
|
142
|
+
description:
|
|
143
|
+
name: yaml
|
|
144
|
+
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
|
145
|
+
url: "https://pub.dev"
|
|
146
|
+
source: hosted
|
|
147
|
+
version: "3.1.3"
|
|
148
|
+
sdks:
|
|
149
|
+
dart: ">=3.13.0 <4.0.0"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Each directory is a complete, runnable Render Workflows service — and also a
|
|
4
|
+
template:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx render-workflows-dart init my-app --template postgres
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
That is deliberate. A template that is not also a working example drifts from
|
|
11
|
+
reality; making them the same thing means every template is one you can run.
|
|
12
|
+
|
|
13
|
+
| | Answers |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| [`default/`](default) | How do I write a task, fan work out, and retry a failure? |
|
|
16
|
+
| [`http/`](http) | How do I call an external API? |
|
|
17
|
+
| [`native/`](native) | How do I read a file, shell out, or use more than one core? |
|
|
18
|
+
| [`postgres/`](postgres) | How do I reach a database? |
|
|
19
|
+
| [`introspect/`](introspect) | How do I inspect Render, or run a task in another workflow? |
|
|
20
|
+
|
|
21
|
+
## Running one
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd http
|
|
25
|
+
npm install
|
|
26
|
+
npx render-dart dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then, in another terminal:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
render workflows tasks list --local
|
|
33
|
+
render workflows start fetchRepo --local --input='["dart-lang/sdk"]'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
No Dart installation is needed — a pinned SDK is fetched into `node_modules`.
|
|
37
|
+
|
|
38
|
+
## Which escape hatch
|
|
39
|
+
|
|
40
|
+
dart2js runs task bodies, and covers most work. It cannot open a file, use a
|
|
41
|
+
second core, or run a package that needs `dart:io`.
|
|
42
|
+
|
|
43
|
+
- **HTTP is not one of those cases.** `package:http` works, because its web
|
|
44
|
+
implementation goes through the platform's networking. See `http/`.
|
|
45
|
+
- **A package shipping WebAssembly** runs in-process with nothing spawned.
|
|
46
|
+
Prefer it when one exists.
|
|
47
|
+
- **Everything else** — files, FFI, databases, real parallelism — is a native
|
|
48
|
+
task. See `native/` and `postgres/`.
|
|
49
|
+
|
|
50
|
+
Native is **not** faster at arithmetic; V8 matches Dart AOT on integer work.
|
|
51
|
+
Choose it for reach, or for parallelism, which is the one real speed win.
|
|
52
|
+
|
|
53
|
+
## A note on dependencies
|
|
54
|
+
|
|
55
|
+
`postgres/seed` and `introspect/` depend on `render_api` by **git**, because it
|
|
56
|
+
is not on pub.dev yet. Those become ordinary version constraints once it is.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# default
|
|
2
|
+
|
|
3
|
+
The Workflows basics, with no dependencies beyond the SDK. This is what
|
|
4
|
+
`render-dart init` produces with no `--template`.
|
|
5
|
+
|
|
6
|
+
| Task | Shows |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `calculateSquare` | A leaf task: arguments in, JSON out |
|
|
9
|
+
| `sumSquares` | Fan-out — each `callTask` is its own run on its own instance |
|
|
10
|
+
| `flaky` | Retry policy, and a failure reaching Render with its real message |
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install && npx render-dart dev
|
|
14
|
+
render workflows start sumSquares --local --input='[[2, 3, 4]]' # 29
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`callTask` starts a **subtask of this workflow**. Reaching a task in a
|
|
18
|
+
*different* service goes through the API instead — see [`../introspect`](../introspect).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dart-workflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "render-dart build",
|
|
7
|
+
"start": "node index.js",
|
|
8
|
+
"dev": "render-dart dev"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@renderinc/sdk": "^0.6.0",
|
|
12
|
+
"render-dart": "^0.7.1"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
}
|
|
17
|
+
}
|