@reventlessdev/reventless-gwt 1.0.0-alpha.100

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 (184) hide show
  1. package/CHANGELOG.md +916 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +569 -0
  10. package/src/Behavior_GWT.res.mjs +523 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +147 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +37 -0
  16. package/src/Cancellation.res.mjs +46 -0
  17. package/src/ChildProcess.res +102 -0
  18. package/src/ChildProcess.res.mjs +107 -0
  19. package/src/Cli.res +905 -0
  20. package/src/Cli.res.mjs +952 -0
  21. package/src/Collector.res +212 -0
  22. package/src/Collector.res.mjs +218 -0
  23. package/src/ComponentMeta.res +77 -0
  24. package/src/ComponentMeta.res.mjs +91 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +99 -0
  33. package/src/DomainDeadCode.res +98 -0
  34. package/src/DomainDeadCode.res.mjs +72 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +45 -0
  40. package/src/Filter.res.mjs +47 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +109 -0
  44. package/src/FormatterHuman.res.mjs +115 -0
  45. package/src/FormatterJson.res +264 -0
  46. package/src/FormatterJson.res.mjs +322 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +100 -0
  50. package/src/FormatterTap.res.mjs +131 -0
  51. package/src/FormatterVsCode.res +352 -0
  52. package/src/FormatterVsCode.res.mjs +546 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +119 -0
  62. package/src/LocalHost.res.mjs +99 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MismatchRender.res +72 -0
  66. package/src/MismatchRender.res.mjs +127 -0
  67. package/src/MultiSourceProjection_GWT.res +316 -0
  68. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  69. package/src/OutboundTranslation_GWT.res +238 -0
  70. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  71. package/src/Outcome.res +97 -0
  72. package/src/Outcome.res.mjs +93 -0
  73. package/src/PackageScan.res +80 -0
  74. package/src/PackageScan.res.mjs +81 -0
  75. package/src/PlatformRunner.res +181 -0
  76. package/src/PlatformRunner.res.mjs +176 -0
  77. package/src/PlatformScan.res +142 -0
  78. package/src/PlatformScan.res.mjs +132 -0
  79. package/src/ProcessManager.res +135 -0
  80. package/src/ProcessManager.res.mjs +119 -0
  81. package/src/Projection_GWT.res +312 -0
  82. package/src/Projection_GWT.res.mjs +332 -0
  83. package/src/Query_GWT.res +379 -0
  84. package/src/Query_GWT.res.mjs +367 -0
  85. package/src/RenderRescript.res +125 -0
  86. package/src/RenderRescript.res.mjs +141 -0
  87. package/src/RunWorker.res +24 -0
  88. package/src/RunWorker.res.mjs +16 -0
  89. package/src/RunnerTypes.res +68 -0
  90. package/src/RunnerTypes.res.mjs +60 -0
  91. package/src/SideEffect_GWT.res +157 -0
  92. package/src/SideEffect_GWT.res.mjs +113 -0
  93. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  94. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  95. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  96. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  97. package/src/StateChange/WithFixtures.res +22 -0
  98. package/src/StateChange/WithFixtures.res.mjs +31 -0
  99. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  100. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  101. package/src/StateChange/WithManualOpen.res +23 -0
  102. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  103. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  104. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  105. package/src/StubRuntime.res +54 -0
  106. package/src/StubRuntime.res.mjs +55 -0
  107. package/src/TestFixtures.res +16 -0
  108. package/src/TestFixtures.res.mjs +34 -0
  109. package/src/Watch.res +108 -0
  110. package/src/Watch.res.mjs +85 -0
  111. package/src/WatcherProbe.res +56 -0
  112. package/src/WatcherProbe.res.mjs +59 -0
  113. package/src/Worker.res +24 -0
  114. package/src/Worker.res.mjs +9 -0
  115. package/tests/AutomationGwtTest.res +71 -0
  116. package/tests/AutomationGwtTest.res.mjs +189 -0
  117. package/tests/BuildClassifierTest.res +133 -0
  118. package/tests/BuildClassifierTest.res.mjs +176 -0
  119. package/tests/CliRunEntryTest.res +57 -0
  120. package/tests/CliRunEntryTest.res.mjs +65 -0
  121. package/tests/CliWatchScopeTest.res +62 -0
  122. package/tests/CliWatchScopeTest.res.mjs +95 -0
  123. package/tests/ComponentMetaTest.res +67 -0
  124. package/tests/ComponentMetaTest.res.mjs +71 -0
  125. package/tests/DelegateGwtTest.res +487 -0
  126. package/tests/DelegateGwtTest.res.mjs +891 -0
  127. package/tests/DiscoveryTest.res +42 -0
  128. package/tests/DiscoveryTest.res.mjs +43 -0
  129. package/tests/DomainDeadCodeTest.res +119 -0
  130. package/tests/DomainDeadCodeTest.res.mjs +158 -0
  131. package/tests/DomainGraphTest.res +448 -0
  132. package/tests/DomainGraphTest.res.mjs +505 -0
  133. package/tests/FlowAggregateGwtTest.res +171 -0
  134. package/tests/FlowAggregateGwtTest.res.mjs +273 -0
  135. package/tests/FlowCrossPluginGwtTest.res +340 -0
  136. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  137. package/tests/FlowGwtTest.res +222 -0
  138. package/tests/FlowGwtTest.res.mjs +433 -0
  139. package/tests/FormatterGoldenTest.res +125 -0
  140. package/tests/FormatterGoldenTest.res.mjs +162 -0
  141. package/tests/FormatterVsCodeMessageTest.res +41 -0
  142. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  143. package/tests/InboundTranslationGwtTest.res +34 -0
  144. package/tests/InboundTranslationGwtTest.res.mjs +99 -0
  145. package/tests/LocalHostIntegration.res +70 -0
  146. package/tests/LocalHostIntegration.res.mjs +64 -0
  147. package/tests/LocalHostTest.res +71 -0
  148. package/tests/LocalHostTest.res.mjs +63 -0
  149. package/tests/MappingGwtTest.res +350 -0
  150. package/tests/MappingGwtTest.res.mjs +596 -0
  151. package/tests/OutboundTranslationGwtTest.res +59 -0
  152. package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
  153. package/tests/PlatformRunnerTest.res +66 -0
  154. package/tests/PlatformRunnerTest.res.mjs +85 -0
  155. package/tests/PlatformScanTest.res +61 -0
  156. package/tests/PlatformScanTest.res.mjs +56 -0
  157. package/tests/QueryGwtTest.res +161 -0
  158. package/tests/QueryGwtTest.res.mjs +314 -0
  159. package/tests/QueryResolverGwtTest.res +71 -0
  160. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  161. package/tests/RunnerUnitTest.res +219 -0
  162. package/tests/RunnerUnitTest.res.mjs +244 -0
  163. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  164. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  165. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  166. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  167. package/tests/SideEffect/MockEmail.res +18 -0
  168. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  169. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  170. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  171. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  172. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  173. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  174. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  175. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  176. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  177. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  178. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  179. package/tests/StateChangeSliceGwtTest.res +160 -0
  180. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  181. package/tests/StateViewSliceGwtTest.res +64 -0
  182. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  183. package/tests/WatchDebounceTest.res +106 -0
  184. package/tests/WatchDebounceTest.res.mjs +128 -0
@@ -0,0 +1,124 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+
5
+ function variantTag(j) {
6
+ if (j === null || Array.isArray(j)) {
7
+ return;
8
+ }
9
+ switch (typeof j) {
10
+ case "string" :
11
+ return j;
12
+ case "object" :
13
+ let match = j["TAG"];
14
+ if (typeof match === "string") {
15
+ return match;
16
+ } else {
17
+ return;
18
+ }
19
+ default:
20
+ return;
21
+ }
22
+ }
23
+
24
+ function forMismatch(sliceOpt, m) {
25
+ let slice = sliceOpt !== undefined ? sliceOpt : "<slice>";
26
+ switch (m.TAG) {
27
+ case "EventsMismatch" :
28
+ return {
29
+ locus: slice + `.decide`,
30
+ branch: undefined,
31
+ message: "decide() returned different events than expected. Check the event payload and count."
32
+ };
33
+ case "ErrorMismatch" :
34
+ let actual = m.actual;
35
+ if (actual === undefined) {
36
+ return {
37
+ locus: slice + `.decide`,
38
+ branch: undefined,
39
+ message: "decide() returned Ok([...]) but the test expected Error."
40
+ };
41
+ }
42
+ let match = variantTag(m.expected);
43
+ let match$1 = variantTag(actual);
44
+ let message = match !== undefined && match$1 !== undefined && match === match$1 ? `decide() returned Error(` + match + `) as expected, but with a different payload.` : "decide() returned a different Error variant than expected.";
45
+ return {
46
+ locus: slice + `.decide`,
47
+ branch: undefined,
48
+ message: message
49
+ };
50
+ case "StateMismatch" :
51
+ return {
52
+ locus: slice + `.evolve`,
53
+ branch: undefined,
54
+ message: "evolve() produced a different state than expected. Check the fold and missing event branches."
55
+ };
56
+ case "NoEventExpected" :
57
+ return {
58
+ locus: slice + `.decide`,
59
+ branch: undefined,
60
+ message: "decide() emitted events but the test expected none — possible idempotency miss."
61
+ };
62
+ case "TodoMismatch" :
63
+ return {
64
+ locus: slice + `.collect / ` + slice + `.resolve`,
65
+ branch: undefined,
66
+ message: "The automation slice's TODO projection diverged from the expected set."
67
+ };
68
+ case "AppendConditionMismatch" :
69
+ return {
70
+ locus: slice + `.commandSchema`,
71
+ branch: undefined,
72
+ message: "DCB optimistic-concurrency condition drift — likely a missing `@s.matches(DcbTag.string)` annotation on a command field, or the expected condition documented in the test disagrees with what the runtime would build."
73
+ };
74
+ case "TranslateError" :
75
+ return {
76
+ locus: slice + `.translate`,
77
+ branch: undefined,
78
+ message: "The translation slice returned a different result than expected."
79
+ };
80
+ case "QueryRowsMismatch" :
81
+ return {
82
+ locus: slice + `.config`,
83
+ branch: undefined,
84
+ message: "The read-model query returned wrong rows — likely a missing index, sub-id, or resolver."
85
+ };
86
+ case "PublishedActionsMismatch" :
87
+ return {
88
+ locus: slice + `.mapOutgoingEvent / ` + slice + `.mapIncomingEvent`,
89
+ branch: undefined,
90
+ message: "The ExtensionPoint mapping / Extension delegate published a different set of events or commands than expected. Check the variant match arms and the one-to-many fan-out."
91
+ };
92
+ case "Throw" :
93
+ return {
94
+ locus: slice,
95
+ branch: undefined,
96
+ message: `The slice threw an exception: ` + m.error
97
+ };
98
+ }
99
+ }
100
+
101
+ function toJson(h) {
102
+ let obj = {};
103
+ obj["locus"] = h.locus;
104
+ obj["branch"] = Stdlib_Option.mapOr(h.branch, null, prim => prim);
105
+ obj["message"] = h.message;
106
+ return obj;
107
+ }
108
+
109
+ function format(h) {
110
+ let b = h.branch;
111
+ if (b !== undefined) {
112
+ return `hint: ` + h.message + `\n Look at ` + h.locus + `'s branch ` + b + `.`;
113
+ } else {
114
+ return `hint: ` + h.message + `\n Look at ` + h.locus + `.`;
115
+ }
116
+ }
117
+
118
+ export {
119
+ variantTag,
120
+ forMismatch,
121
+ toJson,
122
+ format,
123
+ }
124
+ /* No side effect */
@@ -0,0 +1,83 @@
1
+ open ReventlessCore
2
+
3
+ // Minimal inline spec for InboundTranslationSlice. sury-ppx processes the
4
+ // @schema attributes in the same compilation unit as the functor — matches
5
+ // the pattern used by the other GWT DSLs.
6
+ module type SliceSpec = {
7
+ let name: string
8
+
9
+ @schema
10
+ type externalInput
11
+
12
+ @schema
13
+ type command
14
+
15
+ let translate: externalInput => result<array<(string, command)>, string>
16
+ }
17
+
18
+ module type T = {
19
+ module Spec: SliceSpec
20
+
21
+ let describe: (string, unit => unit) => unit
22
+ let test: (string, unit => Outcome.outcome) => unit
23
+
24
+ type translateResult = result<array<(string, Spec.command)>, string>
25
+
26
+ let whenInput: Spec.externalInput => translateResult
27
+ let thenCommands: (translateResult, array<(string, Spec.command)>) => Outcome.outcome
28
+ let thenCommand: (translateResult, string, Spec.command) => Outcome.outcome
29
+ let thenNoCommand: translateResult => Outcome.outcome
30
+ let thenTranslateError: (translateResult, string) => Outcome.outcome
31
+ }
32
+
33
+ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
34
+ module Spec = Spec
35
+
36
+ S.enableJson()
37
+
38
+ let describe = JestBind.describe
39
+ let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
40
+
41
+ type translateResult = result<array<(string, Spec.command)>, string>
42
+
43
+ let encCommand = (c: Spec.command) => c->Message.encode(Spec.commandSchema)
44
+ let encPairs = (pairs: array<(string, Spec.command)>) =>
45
+ pairs->Array.map(((id, cmd)) => {
46
+ let d = Dict.make()
47
+ d->Dict.set("id", JSON.Encode.string(id))
48
+ d->Dict.set("command", encCommand(cmd))
49
+ JSON.Encode.object(d)
50
+ })
51
+
52
+ let whenInput = input => Spec.translate(input)
53
+
54
+ let thenCommands = (result, expected) =>
55
+ switch result {
56
+ | Ok(actual) if actual == expected => Outcome.pass
57
+ | Ok(actual) =>
58
+ Outcome.fail(
59
+ EventsMismatch({expected: encPairs(expected), actual: encPairs(actual)}),
60
+ )
61
+ | Error(msg) =>
62
+ Outcome.fail(TranslateError({expected: "(commands)", actual: Some(msg)}))
63
+ }
64
+
65
+ let thenCommand = (result, expectedId, expectedCmd) =>
66
+ thenCommands(result, [(expectedId, expectedCmd)])
67
+
68
+ let thenNoCommand = result =>
69
+ switch result {
70
+ | Ok([]) => Outcome.pass
71
+ | Ok(actual) => Outcome.fail(NoEventExpected({actual: encPairs(actual)}))
72
+ | Error(msg) =>
73
+ Outcome.fail(TranslateError({expected: "(no commands)", actual: Some(msg)}))
74
+ }
75
+
76
+ let thenTranslateError = (result, expectedMsg) =>
77
+ switch result {
78
+ | Error(actual) if actual == expectedMsg => Outcome.pass
79
+ | Error(actual) =>
80
+ Outcome.fail(TranslateError({expected: expectedMsg, actual: Some(actual)}))
81
+ | Ok(_) => Outcome.fail(TranslateError({expected: expectedMsg, actual: None}))
82
+ }
83
+ }
@@ -0,0 +1,94 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
5
+ import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
6
+ import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
7
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
8
+
9
+ function Make(Spec) {
10
+ S.enableJson();
11
+ let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
12
+ let encPairs = pairs => pairs.map(param => {
13
+ let d = {};
14
+ d["id"] = param[0];
15
+ d["command"] = Message$ReventlessCore.encode(param[1], Spec.commandSchema);
16
+ return d;
17
+ });
18
+ let whenInput = input => Spec.translate(input);
19
+ let thenCommands = (result, expected) => {
20
+ if (result.TAG !== "Ok") {
21
+ return Outcome$ReventlessGwt.fail({
22
+ TAG: "TranslateError",
23
+ expected: "(commands)",
24
+ actual: result._0
25
+ });
26
+ }
27
+ let actual = result._0;
28
+ if (Primitive_object.equal(actual, expected)) {
29
+ return Outcome$ReventlessGwt.pass;
30
+ } else {
31
+ return Outcome$ReventlessGwt.fail({
32
+ TAG: "EventsMismatch",
33
+ expected: encPairs(expected),
34
+ actual: encPairs(actual)
35
+ });
36
+ }
37
+ };
38
+ let thenCommand = (result, expectedId, expectedCmd) => thenCommands(result, [[
39
+ expectedId,
40
+ expectedCmd
41
+ ]]);
42
+ let thenNoCommand = result => {
43
+ if (result.TAG !== "Ok") {
44
+ return Outcome$ReventlessGwt.fail({
45
+ TAG: "TranslateError",
46
+ expected: "(no commands)",
47
+ actual: result._0
48
+ });
49
+ }
50
+ let actual = result._0;
51
+ if (actual.length !== 0) {
52
+ return Outcome$ReventlessGwt.fail({
53
+ TAG: "NoEventExpected",
54
+ actual: encPairs(actual)
55
+ });
56
+ } else {
57
+ return Outcome$ReventlessGwt.pass;
58
+ }
59
+ };
60
+ let thenTranslateError = (result, expectedMsg) => {
61
+ if (result.TAG === "Ok") {
62
+ return Outcome$ReventlessGwt.fail({
63
+ TAG: "TranslateError",
64
+ expected: expectedMsg,
65
+ actual: undefined
66
+ });
67
+ }
68
+ let actual = result._0;
69
+ if (actual === expectedMsg) {
70
+ return Outcome$ReventlessGwt.pass;
71
+ } else {
72
+ return Outcome$ReventlessGwt.fail({
73
+ TAG: "TranslateError",
74
+ expected: expectedMsg,
75
+ actual: actual
76
+ });
77
+ }
78
+ };
79
+ return {
80
+ Spec: Spec,
81
+ describe: JestBind$ReventlessGwt.describe,
82
+ test: test,
83
+ whenInput: whenInput,
84
+ thenCommands: thenCommands,
85
+ thenCommand: thenCommand,
86
+ thenNoCommand: thenNoCommand,
87
+ thenTranslateError: thenTranslateError
88
+ };
89
+ }
90
+
91
+ export {
92
+ Make,
93
+ }
94
+ /* S Not a pure module */
@@ -0,0 +1,70 @@
1
+ // Bridge between the runner-agnostic `Outcome.outcome` algebra and Jest.
2
+ //
3
+ // GWT DSLs emit `Outcome.outcome` from every `then*` combinator. Test files
4
+ // registered via `JestBind.test` / `JestBind.testPromise` have their body's
5
+ // outcome translated into a Jest pass/fail, unless the standalone CLI runner
6
+ // has activated its in-process `Collector` — in which case the call pushes
7
+ // straight into the collector and skips Jest. This lets the same test file
8
+ // run under either `pnpm jest` or `reventless-gwt run` without modification.
9
+ //
10
+ // `~slice` is the slice/component name — each DSL threads `Spec.name` through
11
+ // so failure hints read `Look at AddCategory.decide` rather than the generic
12
+ // `<slice>.decide` fallback.
13
+ //
14
+ // The Jest arm binds directly to Jest's globals via @reventlessdev/rescript-jest
15
+ // (module `JestGlobals`) with throwing semantics: a passing outcome is a no-op,
16
+ // a failing one throws a JS Error whose message Jest reports. (Jest's built-in
17
+ // `fail(msg)` was removed in Jest 27+ ESM mode — the mode these tests run under
18
+ // — so throwing is the portable way to fail a test.)
19
+
20
+ // Translate an outcome into Jest's throwing model: Ok is a no-op, Error throws
21
+ // the formatted mismatch + hint so Jest reports it as the failure message.
22
+ let assertOutcome = (~slice=?, outcome: Outcome.outcome): unit =>
23
+ switch outcome {
24
+ | Ok() => ()
25
+ | Error(m) => {
26
+ let hint = Hint.forMismatch(~slice?, m)
27
+ JsError.throwWithMessage(`${Outcome.format(m)}\n\n${Hint.format(hint)}`)
28
+ }
29
+ }
30
+
31
+ let describe = (label: string, body: unit => unit) =>
32
+ if Collector.isActive() {
33
+ Collector.pushDescribe(label, body)
34
+ } else {
35
+ JestGlobals.describe(label, body)
36
+ }
37
+
38
+ // Pending-spec placeholder emitted by the codegen for slices with no upstream
39
+ // specification. Registers a non-running, non-failing entry under both runners.
40
+ let todo = (label: string) =>
41
+ if Collector.isActive() {
42
+ Collector.pushTodo(label)
43
+ } else {
44
+ JestGlobals.todo(label)
45
+ }
46
+
47
+ let test = (~slice=?, name: string, body: unit => Outcome.outcome) =>
48
+ if Collector.isActive() {
49
+ let location = Collector.captureLocation(1)
50
+ Collector.push(~slice?, ~location?, name, () => Promise.resolve(body()))
51
+ } else {
52
+ JestGlobals.testSync(name, () => assertOutcome(~slice?, body()))
53
+ }
54
+
55
+ let testPromise = (
56
+ ~slice=?,
57
+ name: string,
58
+ ~timeout: option<int>=?,
59
+ body: unit => promise<Outcome.outcome>,
60
+ ) =>
61
+ if Collector.isActive() {
62
+ let location = Collector.captureLocation(1)
63
+ Collector.push(~slice?, ~location?, ~timeout?, name, body)
64
+ } else {
65
+ switch timeout {
66
+ | Some(t) =>
67
+ JestGlobals.testWithTimeout(name, async () => assertOutcome(~slice?, await body()), t)
68
+ | None => JestGlobals.testPromise(name, async () => assertOutcome(~slice?, await body()))
69
+ }
70
+ }
@@ -0,0 +1,63 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
4
+ import * as Hint$ReventlessGwt from "./Hint.res.mjs";
5
+ import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
6
+ import * as Collector$ReventlessGwt from "./Collector.res.mjs";
7
+
8
+ function assertOutcome(slice, outcome) {
9
+ if (outcome.TAG === "Ok") {
10
+ return;
11
+ }
12
+ let m = outcome._0;
13
+ let hint = Hint$ReventlessGwt.forMismatch(slice, m);
14
+ Stdlib_JsError.throwWithMessage(Outcome$ReventlessGwt.format(m) + `\n\n` + Hint$ReventlessGwt.format(hint));
15
+ }
16
+
17
+ function describe(label, body) {
18
+ if (Collector$ReventlessGwt.isActive()) {
19
+ return Collector$ReventlessGwt.pushDescribe(label, body);
20
+ } else {
21
+ globalThis.describe(label, body);
22
+ return;
23
+ }
24
+ }
25
+
26
+ function todo(label) {
27
+ if (Collector$ReventlessGwt.isActive()) {
28
+ return Collector$ReventlessGwt.pushTodo(label);
29
+ } else {
30
+ globalThis.test.todo(label);
31
+ return;
32
+ }
33
+ }
34
+
35
+ function test(slice, name, body) {
36
+ if (Collector$ReventlessGwt.isActive()) {
37
+ let location = Collector$ReventlessGwt.captureLocation(1);
38
+ return Collector$ReventlessGwt.push(slice, location, undefined, name, () => Promise.resolve(body()));
39
+ }
40
+ globalThis.test(name, () => assertOutcome(slice, body()));
41
+ }
42
+
43
+ function testPromise(slice, name, timeout, body) {
44
+ if (!Collector$ReventlessGwt.isActive()) {
45
+ if (timeout !== undefined) {
46
+ globalThis.test(name, async () => assertOutcome(slice, await body()), timeout);
47
+ } else {
48
+ globalThis.test(name, async () => assertOutcome(slice, await body()));
49
+ }
50
+ return;
51
+ }
52
+ let location = Collector$ReventlessGwt.captureLocation(1);
53
+ Collector$ReventlessGwt.push(slice, location, timeout, name, body);
54
+ }
55
+
56
+ export {
57
+ assertOutcome,
58
+ describe,
59
+ todo,
60
+ test,
61
+ testPromise,
62
+ }
63
+ /* No side effect */
package/src/Loader.res ADDED
@@ -0,0 +1,24 @@
1
+ // Dynamic-import wrapper. Each GWT test file registers describe/test at
2
+ // import time, so the caller must `Collector.activate()` before invoking
3
+ // `loadFile` and drain after. The loader converts the absolute path into
4
+ // a `file://` URL so Node's ESM loader accepts it.
5
+ //
6
+ // A monotonic cache-busting query (`?t=N`) is appended so every load
7
+ // re-executes the module's top-level registration. Node's ESM cache is keyed
8
+ // by URL (not mtime), so without this a second load in the same process — the
9
+ // `discover`-then-run within `watch`, and every re-run after a recompile —
10
+ // would hit the cache, skip registration, and drain zero tests. The `?t=`
11
+ // query is stripped from captured stack frames in `Collector.parseFrame`.
12
+
13
+ @module("node:url") external pathToFileURL: string => {"href": string} = "pathToFileURL"
14
+
15
+ let dynamicImport: string => promise<'a> = %raw(`(u) => import(u)`)
16
+
17
+ let counter = ref(0)
18
+
19
+ let loadFile = async (absolutePath: string): unit => {
20
+ counter := counter.contents + 1
21
+ let url = pathToFileURL(absolutePath)
22
+ let busted = url["href"] ++ "?t=" ++ Int.toString(counter.contents)
23
+ let _: 'a = await dynamicImport(busted)
24
+ }
@@ -0,0 +1,23 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodeurl from "node:url";
4
+
5
+ let dynamicImport = ((u) => import(u));
6
+
7
+ let counter = {
8
+ contents: 0
9
+ };
10
+
11
+ async function loadFile(absolutePath) {
12
+ counter.contents = counter.contents + 1 | 0;
13
+ let url = Nodeurl.pathToFileURL(absolutePath);
14
+ let busted = url.href + "?t=" + counter.contents.toString();
15
+ await dynamicImport(busted);
16
+ }
17
+
18
+ export {
19
+ dynamicImport,
20
+ counter,
21
+ loadFile,
22
+ }
23
+ /* node:url Not a pure module */
@@ -0,0 +1,119 @@
1
+ // LocalHost — Phase 4.5 cold local-host foundation (shared by Phases 5, 6, and
2
+ // the Phase 9 runner). Reflects the framework's own domain graph WITHOUT parsing
3
+ // .res: instantiate reventless-local's `Platform.Make()` once, build each compiled
4
+ // `Plugin.res.mjs` against it, read the resolved `pluginStructure` (a plain record
5
+ // — no Pulumi Output), and collect the per-plugin structures.
6
+ //
7
+ // Proven cold (docs/analysis/reventless-vscode-domain-graph-design.md "Spike
8
+ // result"): `Make()` and `Plugin.Make(plat)` open no ports and start no timers —
9
+ // reading `pluginStructure` never touches the bus/scheduler/server wiring that the
10
+ // live `deployPlugin` / `startServers` path adds (that path is the runner's, not
11
+ // this). reventless-local is deliberately NOT a static dependency of reventless-gwt
12
+ // (gwt is upstream of it); the platform module is reached at runtime by absolute
13
+ // path via dynamic import, so a missing local platform is a caller concern, not a
14
+ // compile-time coupling.
15
+
16
+ @module("node:url") external pathToFileURL: string => {"href": string} = "pathToFileURL"
17
+ @module("node:fs") external existsSync: string => bool = "existsSync"
18
+ @module("node:fs") external readFileSync: (string, string) => string = "readFileSync"
19
+ @module("node:path") external join: (string, string) => string = "join"
20
+ @module("node:path") external dirname: string => string = "dirname"
21
+
22
+ type nodeRequire
23
+ @module("node:module") external createRequire: string => nodeRequire = "createRequire"
24
+ @send external requireResolve: (nodeRequire, string) => string = "resolve"
25
+
26
+ let dynamicImport: string => promise<'a> = %raw(`(u) => import(u)`)
27
+
28
+ // Monotonic cache-buster so repeated loads in one process (a watch session
29
+ // re-reading the graph after a recompile) re-execute the composition root rather
30
+ // than hit Node's URL-keyed ESM cache — same rationale as Loader.res. NOTE: only
31
+ // the directly-imported modules are busted; transitively-cached component modules
32
+ // stay warm, so a recompiled *component spec* is not picked up until the next load
33
+ // of the composition root re-imports it. Full transitive invalidation is deferred
34
+ // to the live-update work (Phase 6).
35
+ let counter = ref(0)
36
+ let bustedUrl = (absolutePath: string): string => {
37
+ counter := counter.contents + 1
38
+ pathToFileURL(absolutePath)["href"] ++ "?t=" ++ Int.toString(counter.contents)
39
+ }
40
+
41
+ type pluginRef = {name: string, modulePath: string, packageDir: string}
42
+
43
+ type graph = {
44
+ structures: array<(string, Reventless.Plugin.pluginStructure)>,
45
+ }
46
+
47
+ // Dynamic-import shapes. Untyped at the boundary; reventless-local's Platform and
48
+ // every generated Plugin emit these exact exports.
49
+ type platform
50
+ type localPlatformExports = {"Make": unit => platform}
51
+ type builtPlugin = {"pluginStructure": Reventless.Plugin.pluginStructure}
52
+ type pluginExports = {"Make": platform => builtPlugin}
53
+
54
+ // ── Plugin name derivation — mirrors generator/Config.read exactly so the graph's
55
+ // plugin keys match the names the framework itself bakes into Plugin.res. ──
56
+
57
+ // "@scope/my-catalog" → "MyCatalog", "online-shop" → "OnlineShop".
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
63
+
64
+ let strField = (json, key) =>
65
+ json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
66
+
67
+ let readJson = path =>
68
+ try Some(readFileSync(path, "utf8")->JSON.parseOrThrow) catch {
69
+ | _ => None
70
+ }
71
+
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.
75
+ let derivePluginName = (~pluginSrcDir: string): string => {
76
+ let pluginJson = join(pluginSrcDir, "plugin.json")
77
+ let pluginJsonName =
78
+ existsSync(pluginJson) ? readJson(pluginJson)->Option.flatMap(j => strField(j, "name")) : None
79
+ let packageJsonName =
80
+ readJson(join(dirname(pluginSrcDir), "package.json"))->Option.flatMap(j => strField(j, "name"))
81
+ Reventless.PluginName.resolve(~pluginJsonName, ~packageJsonName)
82
+ }
83
+
84
+ // Given workspace package dirs (e.g. from PackageScan), keep those that carry a
85
+ // compiled composition root and pair each with its framework plugin name.
86
+ let discover = (~packageDirs: array<string>): array<pluginRef> =>
87
+ packageDirs->Array.filterMap(dir => {
88
+ let srcDir = join(dir, "src")
89
+ let modulePath = join(srcDir, "Plugin.res.mjs")
90
+ existsSync(modulePath)
91
+ ? Some({name: derivePluginName(~pluginSrcDir=srcDir), modulePath, packageDir: dir})
92
+ : None
93
+ })
94
+
95
+ // Resolve reventless-local's compiled Platform module from a plugin package's
96
+ // node_modules. reventless-gwt is upstream of reventless-local (no static dep), but
97
+ // every plugin depends on it (`@reventlessdev/reventless-local`), so we resolve the
98
+ // specifier relative to the plugin's package.json. `None` when it isn't installed
99
+ // there — callers then skip platform-dependent features (dead-code / graph).
100
+ let localPlatformSpecifier = "@reventlessdev/reventless-local/src/Platform.res.mjs"
101
+ let resolveLocalPlatform = (~fromPackageDir: string): option<string> =>
102
+ try Some(createRequire(join(fromPackageDir, "package.json"))->requireResolve(localPlatformSpecifier)) catch {
103
+ | _ => None
104
+ }
105
+
106
+ // Cold-load: instantiate the local platform once, build each plugin against it,
107
+ // and read its (already-resolved) pluginStructure.
108
+ let loadGraph = async (~platformModulePath: string, ~plugins: array<pluginRef>): graph => {
109
+ let platMod: localPlatformExports = await dynamicImport(bustedUrl(platformModulePath))
110
+ let plat = platMod["Make"]()
111
+ let structures = []
112
+ for i in 0 to plugins->Array.length - 1 {
113
+ let plugin = plugins->Array.getUnsafe(i)
114
+ let pluginMod: pluginExports = await dynamicImport(bustedUrl(plugin.modulePath))
115
+ let built = pluginMod["Make"](plat)
116
+ structures->Array.push((plugin.name, built["pluginStructure"]))
117
+ }
118
+ {structures: structures}
119
+ }