@reventlessdev/reventless-gwt 1.0.0-alpha.75 → 1.0.0-alpha.77

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/Behavior_GWT.res +125 -159
  4. package/src/Behavior_GWT.res.mjs +156 -59
  5. package/src/Bind.res.mjs +2 -2
  6. package/src/BuildClassifier.res +15 -5
  7. package/src/BuildClassifier.res.mjs +3 -3
  8. package/src/Cancellation.res +6 -0
  9. package/src/Cancellation.res.mjs +3 -2
  10. package/src/ChildProcess.res +19 -1
  11. package/src/ChildProcess.res.mjs +16 -1
  12. package/src/Cli.res +260 -26
  13. package/src/Cli.res.mjs +227 -29
  14. package/src/Collector.res +13 -2
  15. package/src/Collector.res.mjs +8 -5
  16. package/src/ComponentMeta.res +16 -43
  17. package/src/ComponentMeta.res.mjs +12 -41
  18. package/src/Discovery.res +19 -19
  19. package/src/Discovery.res.mjs +5 -9
  20. package/src/DomainGraph.res +0 -0
  21. package/src/DomainGraph.res.mjs +0 -0
  22. package/src/Filter.res +11 -2
  23. package/src/Filter.res.mjs +10 -3
  24. package/src/FormatterHuman.res +14 -43
  25. package/src/FormatterHuman.res.mjs +21 -28
  26. package/src/FormatterJson.res +12 -5
  27. package/src/FormatterJson.res.mjs +8 -6
  28. package/src/FormatterTap.res +13 -52
  29. package/src/FormatterTap.res.mjs +25 -43
  30. package/src/FormatterVsCode.res +4 -19
  31. package/src/FormatterVsCode.res.mjs +4 -23
  32. package/src/JestBind.res +1 -1
  33. package/src/JestBind.res.mjs +2 -2
  34. package/src/LocalHost.res +11 -27
  35. package/src/LocalHost.res.mjs +6 -34
  36. package/src/MismatchRender.res +72 -0
  37. package/src/MismatchRender.res.mjs +127 -0
  38. package/src/Outcome.res +0 -64
  39. package/src/Outcome.res.mjs +0 -47
  40. package/src/PlatformRunner.res +27 -6
  41. package/src/PlatformRunner.res.mjs +27 -6
  42. package/src/PlatformScan.res +14 -10
  43. package/src/PlatformScan.res.mjs +14 -13
  44. package/src/ProcessManager.res +51 -7
  45. package/src/ProcessManager.res.mjs +43 -4
  46. package/src/RunWorker.res +24 -0
  47. package/src/RunWorker.res.mjs +16 -0
  48. package/src/Watch.res +46 -30
  49. package/src/Watch.res.mjs +34 -32
  50. package/src/WatcherProbe.res +11 -3
  51. package/src/WatcherProbe.res.mjs +15 -2
  52. package/src/Worker.res +24 -0
  53. package/src/Worker.res.mjs +9 -0
  54. package/tests/BuildClassifierTest.res +39 -0
  55. package/tests/BuildClassifierTest.res.mjs +44 -0
  56. package/tests/CliRunEntryTest.res +57 -0
  57. package/tests/CliRunEntryTest.res.mjs +65 -0
  58. package/tests/CliWatchScopeTest.res +62 -0
  59. package/tests/CliWatchScopeTest.res.mjs +95 -0
  60. package/tests/ComponentMetaTest.res +15 -0
  61. package/tests/ComponentMetaTest.res.mjs +14 -0
  62. package/tests/DomainGraphTest.res +2 -2
  63. package/tests/FlowAggregateGwtTest.res +2 -0
  64. package/tests/FlowAggregateGwtTest.res.mjs +4 -0
  65. package/tests/FormatterGoldenTest.res +125 -0
  66. package/tests/FormatterGoldenTest.res.mjs +162 -0
  67. package/tests/MappingGwtTest.res +2 -0
  68. package/tests/MappingGwtTest.res.mjs +4 -0
  69. package/tests/WatchDebounceTest.res +35 -7
  70. package/tests/WatchDebounceTest.res.mjs +37 -6
package/src/JestBind.res CHANGED
@@ -60,7 +60,7 @@ let testPromise = (
60
60
  ) =>
61
61
  if Collector.isActive() {
62
62
  let location = Collector.captureLocation(1)
63
- Collector.push(~slice?, ~location?, name, body)
63
+ Collector.push(~slice?, ~location?, ~timeout?, name, body)
64
64
  } else {
65
65
  switch timeout {
66
66
  | Some(t) =>
@@ -35,7 +35,7 @@ function todo(label) {
35
35
  function test(slice, name, body) {
36
36
  if (Collector$ReventlessGwt.isActive()) {
37
37
  let location = Collector$ReventlessGwt.captureLocation(1);
38
- return Collector$ReventlessGwt.push(slice, location, name, () => Promise.resolve(body()));
38
+ return Collector$ReventlessGwt.push(slice, location, undefined, name, () => Promise.resolve(body()));
39
39
  }
40
40
  globalThis.test(name, () => assertOutcome(slice, body()));
41
41
  }
@@ -50,7 +50,7 @@ function testPromise(slice, name, timeout, body) {
50
50
  return;
51
51
  }
52
52
  let location = Collector$ReventlessGwt.captureLocation(1);
53
- Collector$ReventlessGwt.push(slice, location, name, body);
53
+ Collector$ReventlessGwt.push(slice, location, timeout, name, body);
54
54
  }
55
55
 
56
56
  export {
package/src/LocalHost.res CHANGED
@@ -55,23 +55,11 @@ type pluginExports = {"Make": platform => builtPlugin}
55
55
  // plugin keys match the names the framework itself bakes into Plugin.res. ──
56
56
 
57
57
  // "@scope/my-catalog" → "MyCatalog", "online-shop" → "OnlineShop".
58
- let packageNameToPluginName = (packageName: string): string => {
59
- let base = switch packageName->String.split("/") {
60
- | [_scope, local] => local
61
- | [local] => local
62
- | parts => parts->Array.getUnsafe(parts->Array.length - 1)
63
- }
64
- base
65
- ->String.split("-")
66
- ->Array.flatMap(part => part->String.split("_"))
67
- ->Array.map(word =>
68
- word === ""
69
- ? ""
70
- : word->String.slice(~start=0, ~end=1)->String.toUpperCase ++
71
- word->String.slice(~start=1, ~end=word->String.length)
72
- )
73
- ->Array.join("")
74
- }
58
+ // Single-sourced with the plugin generator (spec's `Config`) via
59
+ // `Reventless.PluginName` so the two can't drift — a drift silently breaks
60
+ // graph plugin keys (see the module for the full rationale). Re-exported here
61
+ // under the historical name the tests already pin.
62
+ let packageNameToPluginName = Reventless.PluginName.fromPackageName
75
63
 
76
64
  let strField = (json, key) =>
77
65
  json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
@@ -82,19 +70,15 @@ let readJson = path =>
82
70
  }
83
71
 
84
72
  // plugin.json `name` → else PascalCase(package.json `name`) → else "Plugin".
73
+ // The precedence itself lives in `Reventless.PluginName.resolve`; this only
74
+ // reads the two raw fields with the local node bindings.
85
75
  let derivePluginName = (~pluginSrcDir: string): string => {
86
76
  let pluginJson = join(pluginSrcDir, "plugin.json")
87
- let fromPluginJson =
77
+ let pluginJsonName =
88
78
  existsSync(pluginJson) ? readJson(pluginJson)->Option.flatMap(j => strField(j, "name")) : None
89
- switch fromPluginJson {
90
- | Some(name) => name
91
- | None =>
92
- let pkgJson = join(dirname(pluginSrcDir), "package.json")
93
- readJson(pkgJson)
94
- ->Option.flatMap(j => strField(j, "name"))
95
- ->Option.map(packageNameToPluginName)
96
- ->Option.getOr("Plugin")
97
- }
79
+ let packageJsonName =
80
+ readJson(join(dirname(pluginSrcDir), "package.json"))->Option.flatMap(j => strField(j, "name"))
81
+ Reventless.PluginName.resolve(~pluginJsonName, ~packageJsonName)
98
82
  }
99
83
 
100
84
  // Given workspace package dirs (e.g. from PackageScan), keep those that carry a
@@ -7,6 +7,7 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
7
7
  import * as Nodemodule from "node:module";
8
8
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as PluginName$Reventless from "@reventlessdev/reventless-spec/src/components/PluginName.res.mjs";
10
11
 
11
12
  let dynamicImport = ((u) => import(u));
12
13
 
@@ -19,34 +20,6 @@ function bustedUrl(absolutePath) {
19
20
  return Nodeurl.pathToFileURL(absolutePath).href + "?t=" + counter.contents.toString();
20
21
  }
21
22
 
22
- function packageNameToPluginName(packageName) {
23
- let parts = packageName.split("/");
24
- let len = parts.length;
25
- let base;
26
- if (len >= 3) {
27
- base = parts[parts.length - 1 | 0];
28
- } else {
29
- switch (len) {
30
- case 0 :
31
- base = parts[parts.length - 1 | 0];
32
- break;
33
- case 1 :
34
- base = parts[0];
35
- break;
36
- case 2 :
37
- base = parts[1];
38
- break;
39
- }
40
- }
41
- return base.split("-").flatMap(part => part.split("_")).map(word => {
42
- if (word === "") {
43
- return "";
44
- } else {
45
- return word.slice(0, 1).toUpperCase() + word.slice(1, word.length);
46
- }
47
- }).join("");
48
- }
49
-
50
23
  function strField(json, key) {
51
24
  return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.string);
52
25
  }
@@ -61,12 +34,9 @@ function readJson(path) {
61
34
 
62
35
  function derivePluginName(pluginSrcDir) {
63
36
  let pluginJson = Nodepath.join(pluginSrcDir, "plugin.json");
64
- let fromPluginJson = Nodefs.existsSync(pluginJson) ? Stdlib_Option.flatMap(readJson(pluginJson), j => strField(j, "name")) : undefined;
65
- if (fromPluginJson !== undefined) {
66
- return fromPluginJson;
67
- }
68
- let pkgJson = Nodepath.join(Nodepath.dirname(pluginSrcDir), "package.json");
69
- return Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(readJson(pkgJson), j => strField(j, "name")), packageNameToPluginName), "Plugin");
37
+ let pluginJsonName = Nodefs.existsSync(pluginJson) ? Stdlib_Option.flatMap(readJson(pluginJson), j => strField(j, "name")) : undefined;
38
+ let packageJsonName = Stdlib_Option.flatMap(readJson(Nodepath.join(Nodepath.dirname(pluginSrcDir), "package.json")), j => strField(j, "name"));
39
+ return PluginName$Reventless.resolve(pluginJsonName, packageJsonName);
70
40
  }
71
41
 
72
42
  function discover(packageDirs) {
@@ -111,6 +81,8 @@ async function loadGraph(platformModulePath, plugins) {
111
81
  };
112
82
  }
113
83
 
84
+ let packageNameToPluginName = PluginName$Reventless.fromPackageName;
85
+
114
86
  export {
115
87
  dynamicImport,
116
88
  counter,
@@ -0,0 +1,72 @@
1
+ // Single source for rendering an `Outcome.mismatch`'s expected/actual/extras
2
+ // into RenderRescript strings. The Human and TAP formatters previously each held
3
+ // a full 10-arm switch over the mismatch that computed *identical* rendered
4
+ // values (`RenderRescript.render*`, the `Error()`/`Ok()` wrapping, the Todo
5
+ // `(id, value)` pairs) and differed only in framing — aligned `label: value`
6
+ // lines vs YAML `key: "escaped"` lines. This normalizes the value-rendering once;
7
+ // each formatter interprets the `field` list with its own framing.
8
+ //
9
+ // NOT used by JUnit (renders via the older JSON-based `Outcome.format`) nor
10
+ // FormatterJson/VsCode (they consume the raw JSON values to build a structured
11
+ // `{type, payload, rendered}` + `fieldDiff` payload).
12
+
13
+ // One rendered piece of a mismatch. The constructor carries the *already
14
+ // rendered* string (unescaped — Human uses it raw, TAP yaml-escapes it), except
15
+ // the structural markers `ExpectedNoEvents` (Human prints a literal line; TAP
16
+ // omits it) and `Stack` (Human appends it raw; TAP labels it as a yaml field).
17
+ type field =
18
+ | Expected(string)
19
+ | Actual(string)
20
+ | Key(string)
21
+ | ExpectedNoEvents
22
+ | Error(string)
23
+ | Stack(string)
24
+
25
+ type normalized = {
26
+ kind: string,
27
+ fields: array<field>,
28
+ }
29
+
30
+ let render = RenderRescript.render
31
+ let renderMany = RenderRescript.renderMany
32
+ let renderOption = RenderRescript.renderOption
33
+
34
+ let normalize = (m: Outcome.mismatch): normalized => {
35
+ let fields = switch m {
36
+ | EventsMismatch({expected, actual}) => [Expected(renderMany(expected)), Actual(renderMany(actual))]
37
+ | ErrorMismatch({expected, actual, actualEvents}) => [
38
+ Expected(`Error(${render(expected)})`),
39
+ Actual(
40
+ switch actual {
41
+ | Some(v) => `Error(${render(v)})`
42
+ | None => `Ok(${renderMany(actualEvents)})`
43
+ },
44
+ ),
45
+ ]
46
+ | StateMismatch({key, expected, actual}) => [
47
+ Key(key),
48
+ Expected(renderOption(expected)),
49
+ Actual(renderOption(actual)),
50
+ ]
51
+ | NoEventExpected({actual}) => [ExpectedNoEvents, Actual(renderMany(actual))]
52
+ | TodoMismatch({expected, actual}) =>
53
+ let fmt = (arr: array<(string, JSON.t)>) =>
54
+ "[" ++ arr->Array.map(((id, v)) => `(${id}, ${render(v)})`)->Array.join(", ") ++ "]"
55
+ [Expected(fmt(expected)), Actual(fmt(actual))]
56
+ | AppendConditionMismatch({expected, actual}) => [
57
+ Expected(render(expected)),
58
+ Actual(render(actual)),
59
+ ]
60
+ | TranslateError({expected, actual}) => [
61
+ Expected(expected),
62
+ Actual(actual->Option.getOr("(none)")),
63
+ ]
64
+ | QueryRowsMismatch({expected, actual}) => [Expected(renderMany(expected)), Actual(renderMany(actual))]
65
+ | PublishedActionsMismatch({expected, actual}) => [
66
+ Expected(renderMany(expected)),
67
+ Actual(renderMany(actual)),
68
+ ]
69
+ | Throw({error, stack}) => [Error(error), Stack(stack)]
70
+ }
71
+ {kind: Outcome.kindName(m), fields}
72
+ }
@@ -0,0 +1,127 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
5
+ import * as RenderRescript$ReventlessGwt from "./RenderRescript.res.mjs";
6
+
7
+ function normalize(m) {
8
+ let fields;
9
+ switch (m.TAG) {
10
+ case "ErrorMismatch" :
11
+ let actual = m.actual;
12
+ fields = [
13
+ {
14
+ TAG: "Expected",
15
+ _0: `Error(` + RenderRescript$ReventlessGwt.render(undefined, m.expected) + `)`
16
+ },
17
+ {
18
+ TAG: "Actual",
19
+ _0: actual !== undefined ? `Error(` + RenderRescript$ReventlessGwt.render(undefined, actual) + `)` : `Ok(` + RenderRescript$ReventlessGwt.renderMany(m.actualEvents) + `)`
20
+ }
21
+ ];
22
+ break;
23
+ case "StateMismatch" :
24
+ fields = [
25
+ {
26
+ TAG: "Key",
27
+ _0: m.key
28
+ },
29
+ {
30
+ TAG: "Expected",
31
+ _0: RenderRescript$ReventlessGwt.renderOption(m.expected)
32
+ },
33
+ {
34
+ TAG: "Actual",
35
+ _0: RenderRescript$ReventlessGwt.renderOption(m.actual)
36
+ }
37
+ ];
38
+ break;
39
+ case "NoEventExpected" :
40
+ fields = [
41
+ "ExpectedNoEvents",
42
+ {
43
+ TAG: "Actual",
44
+ _0: RenderRescript$ReventlessGwt.renderMany(m.actual)
45
+ }
46
+ ];
47
+ break;
48
+ case "TodoMismatch" :
49
+ let fmt = arr => "[" + arr.map(param => `(` + param[0] + `, ` + RenderRescript$ReventlessGwt.render(undefined, param[1]) + `)`).join(", ") + "]";
50
+ fields = [
51
+ {
52
+ TAG: "Expected",
53
+ _0: fmt(m.expected)
54
+ },
55
+ {
56
+ TAG: "Actual",
57
+ _0: fmt(m.actual)
58
+ }
59
+ ];
60
+ break;
61
+ case "AppendConditionMismatch" :
62
+ fields = [
63
+ {
64
+ TAG: "Expected",
65
+ _0: RenderRescript$ReventlessGwt.render(undefined, m.expected)
66
+ },
67
+ {
68
+ TAG: "Actual",
69
+ _0: RenderRescript$ReventlessGwt.render(undefined, m.actual)
70
+ }
71
+ ];
72
+ break;
73
+ case "TranslateError" :
74
+ fields = [
75
+ {
76
+ TAG: "Expected",
77
+ _0: m.expected
78
+ },
79
+ {
80
+ TAG: "Actual",
81
+ _0: Stdlib_Option.getOr(m.actual, "(none)")
82
+ }
83
+ ];
84
+ break;
85
+ case "Throw" :
86
+ fields = [
87
+ {
88
+ TAG: "Error",
89
+ _0: m.error
90
+ },
91
+ {
92
+ TAG: "Stack",
93
+ _0: m.stack
94
+ }
95
+ ];
96
+ break;
97
+ default:
98
+ fields = [
99
+ {
100
+ TAG: "Expected",
101
+ _0: RenderRescript$ReventlessGwt.renderMany(m.expected)
102
+ },
103
+ {
104
+ TAG: "Actual",
105
+ _0: RenderRescript$ReventlessGwt.renderMany(m.actual)
106
+ }
107
+ ];
108
+ }
109
+ return {
110
+ kind: Outcome$ReventlessGwt.kindName(m),
111
+ fields: fields
112
+ };
113
+ }
114
+
115
+ let render = RenderRescript$ReventlessGwt.render;
116
+
117
+ let renderMany = RenderRescript$ReventlessGwt.renderMany;
118
+
119
+ let renderOption = RenderRescript$ReventlessGwt.renderOption;
120
+
121
+ export {
122
+ render,
123
+ renderMany,
124
+ renderOption,
125
+ normalize,
126
+ }
127
+ /* No side effect */
package/src/Outcome.res CHANGED
@@ -95,67 +95,3 @@ let format = (m: mismatch) =>
95
95
  )}`
96
96
  | Throw({error, stack}) => `Throw: ${error}\n${stack}`
97
97
  }
98
-
99
- // Structured JSON payload per §3.3 of the analysis.
100
- // Closed schema — consumers switch on the `kind` discriminator.
101
- // Stage 7 enriches with `rendered` strings and `fieldDiff`.
102
- let toJson = (m: mismatch): JSON.t => {
103
- let obj = Dict.make()
104
- obj->Dict.set("kind", JSON.Encode.string(kindName(m)))
105
- let jsonArr = arr => JSON.Encode.array(arr)
106
- let optJson = o => o->Option.getOr(JSON.Encode.null)
107
- switch m {
108
- | EventsMismatch({expected, actual}) => {
109
- obj->Dict.set("expected", jsonArr(expected))
110
- obj->Dict.set("actual", jsonArr(actual))
111
- }
112
- | ErrorMismatch({expected, actual, actualEvents}) => {
113
- obj->Dict.set("expected", expected)
114
- obj->Dict.set("actual", optJson(actual))
115
- obj->Dict.set("actualEvents", jsonArr(actualEvents))
116
- }
117
- | StateMismatch({key, expected, actual}) => {
118
- obj->Dict.set("key", JSON.Encode.string(key))
119
- obj->Dict.set("expected", optJson(expected))
120
- obj->Dict.set("actual", optJson(actual))
121
- }
122
- | NoEventExpected({actual}) => obj->Dict.set("actual", jsonArr(actual))
123
- | TodoMismatch({expected, actual}) => {
124
- let pairArr = pairs =>
125
- pairs
126
- ->Array.map(((id, v)) => {
127
- let d = Dict.make()
128
- d->Dict.set("id", JSON.Encode.string(id))
129
- d->Dict.set("value", v)
130
- JSON.Encode.object(d)
131
- })
132
- ->jsonArr
133
- obj->Dict.set("expected", pairArr(expected))
134
- obj->Dict.set("actual", pairArr(actual))
135
- }
136
- | AppendConditionMismatch({expected, actual}) => {
137
- obj->Dict.set("expected", expected)
138
- obj->Dict.set("actual", actual)
139
- }
140
- | TranslateError({expected, actual}) => {
141
- obj->Dict.set("expected", JSON.Encode.string(expected))
142
- obj->Dict.set(
143
- "actual",
144
- actual->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
145
- )
146
- }
147
- | QueryRowsMismatch({expected, actual}) => {
148
- obj->Dict.set("expected", jsonArr(expected))
149
- obj->Dict.set("actual", jsonArr(actual))
150
- }
151
- | PublishedActionsMismatch({expected, actual}) => {
152
- obj->Dict.set("expected", jsonArr(expected))
153
- obj->Dict.set("actual", jsonArr(actual))
154
- }
155
- | Throw({error, stack}) => {
156
- obj->Dict.set("error", JSON.Encode.string(error))
157
- obj->Dict.set("stack", JSON.Encode.string(stack))
158
- }
159
- }
160
- JSON.Encode.object(obj)
161
- }
@@ -76,52 +76,6 @@ function format(m) {
76
76
  }
77
77
  }
78
78
 
79
- function toJson(m) {
80
- let obj = {};
81
- obj["kind"] = kindName(m);
82
- switch (m.TAG) {
83
- case "ErrorMismatch" :
84
- obj["expected"] = m.expected;
85
- obj["actual"] = Stdlib_Option.getOr(m.actual, null);
86
- obj["actualEvents"] = m.actualEvents;
87
- break;
88
- case "StateMismatch" :
89
- obj["key"] = m.key;
90
- obj["expected"] = Stdlib_Option.getOr(m.expected, null);
91
- obj["actual"] = Stdlib_Option.getOr(m.actual, null);
92
- break;
93
- case "NoEventExpected" :
94
- obj["actual"] = m.actual;
95
- break;
96
- case "TodoMismatch" :
97
- let pairArr = pairs => pairs.map(param => {
98
- let d = {};
99
- d["id"] = param[0];
100
- d["value"] = param[1];
101
- return d;
102
- });
103
- obj["expected"] = pairArr(m.expected);
104
- obj["actual"] = pairArr(m.actual);
105
- break;
106
- case "TranslateError" :
107
- obj["expected"] = m.expected;
108
- obj["actual"] = Stdlib_Option.mapOr(m.actual, null, prim => prim);
109
- break;
110
- case "EventsMismatch" :
111
- case "AppendConditionMismatch" :
112
- case "QueryRowsMismatch" :
113
- case "PublishedActionsMismatch" :
114
- obj["expected"] = m.expected;
115
- obj["actual"] = m.actual;
116
- break;
117
- case "Throw" :
118
- obj["error"] = m.error;
119
- obj["stack"] = m.stack;
120
- break;
121
- }
122
- return obj;
123
- }
124
-
125
79
  let pass = {
126
80
  TAG: "Ok",
127
81
  _0: undefined
@@ -135,6 +89,5 @@ export {
135
89
  stringifyOptJson,
136
90
  stringifyJsonArray,
137
91
  format,
138
- toJson,
139
92
  }
140
93
  /* No side effect */
@@ -60,6 +60,9 @@ let openEphemeral = (): promise<(netServer, int)> =>
60
60
  s->_listen(0, "127.0.0.1", () => resolve((s, (s->_address)["port"])))
61
61
  })
62
62
 
63
+ let closeServer = (s: netServer): promise<unit> =>
64
+ Promise.make((resolve, _reject) => s->_close(() => resolve()))
65
+
63
66
  // Hold all n open while collecting ports so they're guaranteed distinct, then
64
67
  // release them just before the child binds.
65
68
  let allocFreePorts = async (n: int): array<int> => {
@@ -70,7 +73,10 @@ let allocFreePorts = async (n: int): array<int> => {
70
73
  servers->Array.push(s)
71
74
  ports->Array.push(p)
72
75
  }
73
- servers->Array.forEach(s => s->_close(() => ()))
76
+ // Await every close so the ports are actually released before the child binds
77
+ // them. Closing fire-and-forget returned the ports while the sockets were
78
+ // still bound, racing the child's bind into EADDRINUSE.
79
+ let _ = await Promise.all(servers->Array.map(closeServer))
74
80
  ports
75
81
  }
76
82
 
@@ -150,11 +156,26 @@ let run = async (
150
156
  }
151
157
  ChildProcess.onStdoutLine(proc, handle)
152
158
  ChildProcess.onStderrLine(proc, handle)
153
- ChildProcess.onClose(proc, code => callbacks.onStop(code))
154
159
 
155
- // Keep the process alive until cancelled like runWatch, returning here would
156
- // let the bin wrapper process.exit and tear down the child immediately.
157
- await Promise.make((resolve, _reject) => Cancellation.onCancel(() => resolve()))
158
- 0
160
+ // Keep the parent alive until either the operator cancels or the child exits.
161
+ // The previous shape awaited cancellation *forever* and always returned 0, so
162
+ // a crashed platform child left a zombie parent that reported success. Resolve
163
+ // on child close (or a spawn error) too, and surface the child's exit code.
164
+ let exitCode = ref(0)
165
+ await Promise.make((resolve, _reject) => {
166
+ Cancellation.onCancel(() => resolve())
167
+ ChildProcess.onError(proc, msg => {
168
+ callbacks.onLog("platform spawn error: " ++ msg)
169
+ callbacks.onStop(None)
170
+ exitCode := 1
171
+ resolve()
172
+ })
173
+ ChildProcess.onClose(proc, code => {
174
+ callbacks.onStop(code)
175
+ exitCode := code->Option.getOr(0)
176
+ resolve()
177
+ })
178
+ })
179
+ exitCode.contents
159
180
  }
160
181
  }
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Nodenet from "node:net";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
5
  import * as Cancellation$ReventlessGwt from "./Cancellation.res.mjs";
5
6
  import * as ChildProcess$ReventlessGwt from "./ChildProcess.res.mjs";
6
7
  import * as PlatformScan$ReventlessGwt from "./PlatformScan.res.mjs";
@@ -44,6 +45,12 @@ function openEphemeral() {
44
45
  });
45
46
  }
46
47
 
48
+ function closeServer(s) {
49
+ return new Promise((resolve, _reject) => {
50
+ s.close(() => resolve());
51
+ });
52
+ }
53
+
47
54
  async function allocFreePorts(n) {
48
55
  let servers = [];
49
56
  let ports = [];
@@ -52,9 +59,7 @@ async function allocFreePorts(n) {
52
59
  servers.push(match[0]);
53
60
  ports.push(match[1]);
54
61
  }
55
- servers.forEach(s => {
56
- s.close(() => {});
57
- });
62
+ await Promise.all(servers.map(closeServer));
58
63
  return ports;
59
64
  }
60
65
 
@@ -136,9 +141,24 @@ async function run(roots, backend, fixedPortsOpt, callbacks) {
136
141
  };
137
142
  ChildProcess$ReventlessGwt.onStdoutLine(proc, handle);
138
143
  ChildProcess$ReventlessGwt.onStderrLine(proc, handle);
139
- ChildProcess$ReventlessGwt.onClose(proc, code => callbacks.onStop(code));
140
- await new Promise((resolve, _reject) => Cancellation$ReventlessGwt.onCancel(() => resolve()));
141
- return 0;
144
+ let exitCode = {
145
+ contents: 0
146
+ };
147
+ await new Promise((resolve, _reject) => {
148
+ Cancellation$ReventlessGwt.onCancel(() => resolve());
149
+ ChildProcess$ReventlessGwt.onError(proc, msg => {
150
+ callbacks.onLog("platform spawn error: " + msg);
151
+ callbacks.onStop(undefined);
152
+ exitCode.contents = 1;
153
+ resolve();
154
+ });
155
+ ChildProcess$ReventlessGwt.onClose(proc, code => {
156
+ callbacks.onStop(code);
157
+ exitCode.contents = Stdlib_Option.getOr(code, 0);
158
+ resolve();
159
+ });
160
+ });
161
+ return exitCode.contents;
142
162
  }
143
163
  callbacks.onLog("no reventless-local platform package (src/Main.res.mjs + @reventlessdev/reventless-local) found under: " + roots.join(", "));
144
164
  return 1;
@@ -148,6 +168,7 @@ export {
148
168
  tapSentinel,
149
169
  classifyLine,
150
170
  openEphemeral,
171
+ closeServer,
151
172
  allocFreePorts,
152
173
  fixedPlatformPorts,
153
174
  run,
@@ -105,16 +105,20 @@ let inspectDir = (dir: string): option<platformPkg> => {
105
105
  // declaration order preserved). Prunes node_modules/lib/.git like Discovery.
106
106
  let rec walk = async (dir: string, acc: array<platformPkg>): array<platformPkg> => {
107
107
  switch inspectDir(dir) {
108
- | Some(pkg) => acc->Array.push(pkg) // a platform package — record, don't descend into its src
109
- | None => ()
110
- }
111
- let entries = try await _readdir(dir, {withFileTypes: true}) catch {
112
- | _ => []
113
- }
114
- for i in 0 to entries->Array.length - 1 {
115
- let entry = entries->Array.getUnsafe(i)
116
- if entry._isDirectory() && !Array.includes(ignoreNames, entry.name) {
117
- let _ = await walk(join(dir, entry.name), acc)
108
+ // A platform package — record it and stop: descending into its own subtree
109
+ // (src/, generated code, nested workspace copies) can only waste I/O or turn
110
+ // up spurious inner matches. This is what the "don't descend" comment always
111
+ // intended; the recursion below used to run regardless.
112
+ | Some(pkg) => acc->Array.push(pkg)
113
+ | None =>
114
+ let entries = try await _readdir(dir, {withFileTypes: true}) catch {
115
+ | _ => []
116
+ }
117
+ for i in 0 to entries->Array.length - 1 {
118
+ let entry = entries->Array.getUnsafe(i)
119
+ if entry._isDirectory() && !Array.includes(ignoreNames, entry.name) {
120
+ let _ = await walk(join(dir, entry.name), acc)
121
+ }
118
122
  }
119
123
  }
120
124
  acc
@@ -83,19 +83,20 @@ async function walk(dir, acc) {
83
83
  let pkg = inspectDir(dir);
84
84
  if (pkg !== undefined) {
85
85
  acc.push(pkg);
86
- }
87
- let entries;
88
- try {
89
- entries = await Promises.readdir(dir, {
90
- withFileTypes: true
91
- });
92
- } catch (exn) {
93
- entries = [];
94
- }
95
- for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
96
- let entry = entries[i];
97
- if (entry.isDirectory() && !ignoreNames.includes(entry.name)) {
98
- await walk(Nodepath.join(dir, entry.name), acc);
86
+ } else {
87
+ let entries;
88
+ try {
89
+ entries = await Promises.readdir(dir, {
90
+ withFileTypes: true
91
+ });
92
+ } catch (exn) {
93
+ entries = [];
94
+ }
95
+ for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
96
+ let entry = entries[i];
97
+ if (entry.isDirectory() && !ignoreNames.includes(entry.name)) {
98
+ await walk(Nodepath.join(dir, entry.name), acc);
99
+ }
99
100
  }
100
101
  }
101
102
  return acc;