@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,17 @@
1
+ // Implementation half — pairs with [WithManualOpen.res].
2
+
3
+ open WithManualOpen
4
+
5
+ type state = {exists: bool}
6
+ let initialState: state = {exists: false}
7
+
8
+ let evolve = (_state: state, event: consumedEvent): state =>
9
+ switch event {
10
+ | CategoryAdded => {exists: true}
11
+ }
12
+
13
+ let decide = (state: state, command: command): result<array<event>, error> =>
14
+ switch command {
15
+ | AddCategory({categoryId, name}) =>
16
+ state.exists ? Error(CategoryAlreadyExists) : Ok([CategoryAdded({categoryId, name})])
17
+ }
@@ -0,0 +1,37 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function evolve(_state, event) {
5
+ return {
6
+ exists: true
7
+ };
8
+ }
9
+
10
+ function decide(state, command) {
11
+ if (state.exists) {
12
+ return {
13
+ TAG: "Error",
14
+ _0: "CategoryAlreadyExists"
15
+ };
16
+ } else {
17
+ return {
18
+ TAG: "Ok",
19
+ _0: [{
20
+ TAG: "CategoryAdded",
21
+ categoryId: command.categoryId,
22
+ name: command.name
23
+ }]
24
+ };
25
+ }
26
+ }
27
+
28
+ let initialState = {
29
+ exists: false
30
+ };
31
+
32
+ export {
33
+ initialState,
34
+ evolve,
35
+ decide,
36
+ }
37
+ /* No side effect */
@@ -0,0 +1,54 @@
1
+ // Inert runtime arguments shared by the Delegate / Flow GWT DSLs.
2
+ //
3
+ // ExtensionPoint mappings and Extension delegates are pure translations, but
4
+ // their user-level functions take infrastructure handles (a query engine, a
5
+ // scheduler, message metadata, the plugin definition). The GWT runs them with
6
+ // these no-op stubs so the translation logic is exercised without any real
7
+ // infrastructure. See `docs/plans/done/gwt-flow-and-extension-test-kinds.md` Phase 0.
8
+
9
+ // No-op QueryEngine — any DB lookup resolves to empty results. Kept identical
10
+ // to the inline stub `Mapping_GWT` defines.
11
+ let queryEngine: Reventless.QueryEngine.operations = {
12
+ scan: (~readModelName as _, ~filterConfigs as _, ~limit as _) => []->Promise.resolve,
13
+ query: (
14
+ ~readModelName as _: string,
15
+ ~key as _: option<string>=?,
16
+ ~id as _: Reventless.QueryEngine.value,
17
+ ~subIdConfig as _: option<Reventless.QueryEngine.SubId.config>=?,
18
+ ~filterConfigs as _: option<array<Reventless.QueryEngine.Filter.config>>=?,
19
+ ~ascending as _: option<bool>=?,
20
+ ~limit as _: option<int>=?,
21
+ ) => []->Promise.resolve,
22
+ }
23
+
24
+ // Scheduler stubs — `create` / `delete` are inert. EP/Extension `Call`
25
+ // directives receive these as the side-effect handles they never act on.
26
+ let createSchedule: Reventless.Schedule.create = _schedule => Promise.resolve()
27
+ let deleteSchedule: Reventless.Schedule.delete = _name => Promise.resolve()
28
+
29
+ // Default message metadata threaded into the user-level mapping functions.
30
+ let meta: Reventless.Message.meta = {
31
+ service: "gwt",
32
+ time: "1970-01-01T00:00:00Z",
33
+ msgId: "gwt-msg",
34
+ correlationId: "gwt-corr",
35
+ }
36
+
37
+ // Minimal plugin definition — Extension delegates receive it as an inert arg.
38
+ // Mirrors the `fakePluginDefinition` the platform admin uses for its internal
39
+ // callback wiring.
40
+ let pluginDefinition: Reventless.Plugin.pluginDefinition = {
41
+ id: "Gwt@TEST",
42
+ name: "Gwt",
43
+ version: "TEST",
44
+ extensionPoints: [],
45
+ extensions: [],
46
+ eventCollector: "NOT-SET",
47
+ extensionProtocols: [],
48
+ apiSchemaFragment: None,
49
+ apiTarget: None,
50
+ uiFragments: None,
51
+ structure: None,
52
+ dcbEventLog: None,
53
+ kind: Domain,
54
+ }
@@ -0,0 +1,55 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function queryEngine_scan(param, param$1, param$2) {
5
+ return Promise.resolve([]);
6
+ }
7
+
8
+ function queryEngine_query(param, param$1, param$2, param$3, param$4, param$5, param$6) {
9
+ return Promise.resolve([]);
10
+ }
11
+
12
+ let queryEngine = {
13
+ scan: queryEngine_scan,
14
+ query: queryEngine_query
15
+ };
16
+
17
+ function createSchedule(_schedule) {
18
+ return Promise.resolve();
19
+ }
20
+
21
+ function deleteSchedule(_name) {
22
+ return Promise.resolve();
23
+ }
24
+
25
+ let pluginDefinition = {
26
+ id: "Gwt@TEST",
27
+ name: "Gwt",
28
+ version: "TEST",
29
+ extensionPoints: [],
30
+ extensions: [],
31
+ eventCollector: "NOT-SET",
32
+ extensionProtocols: [],
33
+ apiSchemaFragment: undefined,
34
+ apiTarget: undefined,
35
+ uiFragments: undefined,
36
+ structure: undefined,
37
+ dcbEventLog: undefined,
38
+ kind: "Domain"
39
+ };
40
+
41
+ let meta = {
42
+ service: "gwt",
43
+ time: "1970-01-01T00:00:00Z",
44
+ msgId: "gwt-msg",
45
+ correlationId: "gwt-corr"
46
+ };
47
+
48
+ export {
49
+ queryEngine,
50
+ createSchedule,
51
+ deleteSchedule,
52
+ meta,
53
+ pluginDefinition,
54
+ }
55
+ /* No side effect */
@@ -0,0 +1,16 @@
1
+ let id = "id"
2
+ let meta = {
3
+ Reventless.Message.service: "service",
4
+ user: "ProjectionTest",
5
+ ip: "ip",
6
+ time: "time",
7
+ msgId: "msgId",
8
+ correlationId: "correlationId",
9
+ }
10
+
11
+ let context = {Reventless.Message.meta, id}
12
+
13
+ let statusChange = {
14
+ Reventless.Message.at: context.meta.time,
15
+ by: context.meta.user->Option.getOr(""),
16
+ }
@@ -0,0 +1,34 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+
5
+ let id = "id";
6
+
7
+ let meta = {
8
+ service: "service",
9
+ time: "time",
10
+ ip: "ip",
11
+ user: "ProjectionTest",
12
+ msgId: "msgId",
13
+ correlationId: "correlationId"
14
+ };
15
+
16
+ let context = {
17
+ id: id,
18
+ meta: meta
19
+ };
20
+
21
+ let statusChange_by = Stdlib_Option.getOr("ProjectionTest", "");
22
+
23
+ let statusChange = {
24
+ at: "time",
25
+ by: statusChange_by
26
+ };
27
+
28
+ export {
29
+ id,
30
+ meta,
31
+ context,
32
+ statusChange,
33
+ }
34
+ /* statusChange Not a pure module */
package/src/Watch.res ADDED
@@ -0,0 +1,108 @@
1
+ // Wraps `chokidar` for the `reventless-gwt watch` and `reventless-gwt run
2
+ // --watch` modes. Emits one callback per file change event (creates, edits,
3
+ // deletes) after a small debounce window so rapid editor saves don't trigger
4
+ // overlapping runs.
5
+
6
+ type watcher
7
+ type opts = {
8
+ ignoreInitial: bool,
9
+ ignored: array<string>,
10
+ }
11
+
12
+ // The kind of filesystem change, threaded through so the consumer can tell a
13
+ // *structural* change (a source file appearing/disappearing — the move/delete
14
+ // half of a relocation) from a plain edit. Phase 12 keys its clean-rebuild on
15
+ // an `Unlink` of a `.res` source.
16
+ type event = Add | Change | Unlink
17
+
18
+ @module("chokidar") external watch: (array<string>, opts) => watcher = "watch"
19
+ @send external on: (watcher, string, string => unit) => watcher = "on"
20
+ @send external close: watcher => promise<unit> = "close"
21
+
22
+ @val external setTimeout: (unit => unit, int) => float = "setTimeout"
23
+ @val external clearTimeout: float => unit = "clearTimeout"
24
+
25
+ // True when an event is the *structural* signal Phase 12 acts on: a source
26
+ // `.res` file leaving a directory (the move/delete half of a relocation).
27
+ // Generated `.res.mjs` outputs end in `.mjs`, so they never match — which is
28
+ // also what keeps a `rescript clean` (it deletes those outputs) from
29
+ // re-triggering the clean-rebuild it just ran.
30
+ let isStructuralSource = (event: event, path: string): bool =>
31
+ event == Unlink && String.endsWith(path, ".res")
32
+
33
+ // Coalesce a burst of chokidar events into a single debounce window, then emit.
34
+ // A window may span *multiple* packages — a branch switch or a multi-package
35
+ // refactor emits `.res` unlinks under several package roots at once. The old
36
+ // shape kept only one "best" path, so it clean-rebuilt just one package and
37
+ // left the others with stale `.res.mjs`. Instead accumulate the *set* of
38
+ // distinct structural source paths and emit one `Unlink` callback per path
39
+ // (the consumer maps each to its owning package). If the window held no
40
+ // structural signal, emit a single representative event so the consumer does a
41
+ // plain re-run.
42
+ let debounce = (wait: int, fn: (event, string) => unit) => {
43
+ let pending: ref<option<float>> = ref(None)
44
+ let structural: ref<array<string>> = ref([])
45
+ let sawOther = ref(false)
46
+ let otherEvent = ref(Change)
47
+ let otherPath = ref("")
48
+ (event, path) => {
49
+ if isStructuralSource(event, path) {
50
+ if !(structural.contents->Array.includes(path)) {
51
+ structural := Array.concat(structural.contents, [path])
52
+ }
53
+ } else {
54
+ sawOther := true
55
+ otherEvent := event
56
+ otherPath := path
57
+ }
58
+ switch pending.contents {
59
+ | Some(t) => clearTimeout(t)
60
+ | None => ()
61
+ }
62
+ pending :=
63
+ Some(
64
+ setTimeout(() => {
65
+ pending := None
66
+ let paths = structural.contents
67
+ let other = sawOther.contents
68
+ let oe = otherEvent.contents
69
+ let op = otherPath.contents
70
+ structural := []
71
+ sawOther := false
72
+ otherEvent := Change
73
+ otherPath := ""
74
+ if paths->Array.length > 0 {
75
+ // One rebuild per distinct owning package; a plain edit alongside
76
+ // is covered by each structural rebuild's own re-run.
77
+ paths->Array.forEach(p => fn(Unlink, p))
78
+ } else if other {
79
+ fn(oe, op)
80
+ }
81
+ }, wait),
82
+ )
83
+ }
84
+ }
85
+
86
+ let start = (roots: array<string>, onChange: (event, string) => unit): watcher => {
87
+ let w = watch(
88
+ roots,
89
+ {
90
+ ignoreInitial: true,
91
+ // Keep in sync with Discovery's pruned dirs — writes under `dist/` or
92
+ // `.history/` are build/editor output, not source, and a `dist/` write
93
+ // could otherwise drive a re-run loop.
94
+ ignored: [
95
+ "**/node_modules/**",
96
+ "**/lib/**",
97
+ "**/.git/**",
98
+ "**/dist/**",
99
+ "**/.history/**",
100
+ ],
101
+ },
102
+ )
103
+ let debounced = debounce(120, onChange)
104
+ w
105
+ ->on("add", p => debounced(Add, p))
106
+ ->on("change", p => debounced(Change, p))
107
+ ->on("unlink", p => debounced(Unlink, p))
108
+ }
@@ -0,0 +1,85 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Chokidar from "chokidar";
4
+
5
+ function isStructuralSource(event, path) {
6
+ if (event === "Unlink") {
7
+ return path.endsWith(".res");
8
+ } else {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ function debounce(wait, fn) {
14
+ let pending = {
15
+ contents: undefined
16
+ };
17
+ let structural = {
18
+ contents: []
19
+ };
20
+ let sawOther = {
21
+ contents: false
22
+ };
23
+ let otherEvent = {
24
+ contents: "Change"
25
+ };
26
+ let otherPath = {
27
+ contents: ""
28
+ };
29
+ return (event, path) => {
30
+ if (isStructuralSource(event, path)) {
31
+ if (!structural.contents.includes(path)) {
32
+ structural.contents = structural.contents.concat([path]);
33
+ }
34
+ } else {
35
+ sawOther.contents = true;
36
+ otherEvent.contents = event;
37
+ otherPath.contents = path;
38
+ }
39
+ let t = pending.contents;
40
+ if (t !== undefined) {
41
+ clearTimeout(t);
42
+ }
43
+ pending.contents = setTimeout(() => {
44
+ pending.contents = undefined;
45
+ let paths = structural.contents;
46
+ let other = sawOther.contents;
47
+ let oe = otherEvent.contents;
48
+ let op = otherPath.contents;
49
+ structural.contents = [];
50
+ sawOther.contents = false;
51
+ otherEvent.contents = "Change";
52
+ otherPath.contents = "";
53
+ if (paths.length !== 0) {
54
+ paths.forEach(p => fn("Unlink", p));
55
+ return;
56
+ } else if (other) {
57
+ return fn(oe, op);
58
+ } else {
59
+ return;
60
+ }
61
+ }, wait);
62
+ };
63
+ }
64
+
65
+ function start(roots, onChange) {
66
+ let w = Chokidar.watch(roots, {
67
+ ignoreInitial: true,
68
+ ignored: [
69
+ "**/node_modules/**",
70
+ "**/lib/**",
71
+ "**/.git/**",
72
+ "**/dist/**",
73
+ "**/.history/**"
74
+ ]
75
+ });
76
+ let debounced = debounce(120, onChange);
77
+ return w.on("add", p => debounced("Add", p)).on("change", p => debounced("Change", p)).on("unlink", p => debounced("Unlink", p));
78
+ }
79
+
80
+ export {
81
+ isStructuralSource,
82
+ debounce,
83
+ start,
84
+ }
85
+ /* chokidar Not a pure module */
@@ -0,0 +1,56 @@
1
+ // Detects whether a ReScript build watcher is already running for a package, so
2
+ // the watch manager can defer to a developer's (or rescript-vscode's) own
3
+ // watcher rather than spawn a duplicate. ReScript records the building process's
4
+ // PID in a lock file and liveness-checks it (a dead-pid lock is taken over), so
5
+ // the reliable signal is: lock present AND its pid is alive. A stale lock is
6
+ // harmless — rescript self-heals it on the next build.
7
+ //
8
+ // Two lock files exist depending on which command form started the build:
9
+ // - `lib/watch.lock` — canonical `rescript watch` (used by rescript-vscode
10
+ // ≥ 1.73's "Start Build", and the non-deprecated form)
11
+ // - `lib/rescript.lock` — legacy `rescript build -w` (what `pnpm run start`
12
+ // still maps to in the example packages)
13
+ // Both must be checked: missing `watch.lock` is exactly how the rescript-vscode
14
+ // watcher slipped past an earlier rescript.lock-only probe, causing a doomed
15
+ // duplicate spawn.
16
+
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
+ @val external _processKillSig: (int, int) => unit = "process.kill"
21
+
22
+ // `process.kill(pid, 0)` performs no signal delivery — it only probes existence.
23
+ // It throws `ESRCH` when the pid is gone (dead) and `EPERM` when the pid exists
24
+ // but is owned by another user (alive, just not ours). A bare catch-all treated
25
+ // EPERM as dead, which would spawn a duplicate watcher over a live foreign one;
26
+ // treat EPERM as alive.
27
+ let _errCode: JsExn.t => option<string> = %raw(`(e) => (e && typeof e.code === "string") ? e.code : undefined`)
28
+ let isAlive = (pid: int): bool =>
29
+ try {
30
+ _processKillSig(pid, 0)
31
+ true
32
+ } catch {
33
+ | exn =>
34
+ switch JsExn.fromException(exn) {
35
+ | Some(e) => _errCode(e) == Some("EPERM")
36
+ | None => false
37
+ }
38
+ }
39
+
40
+ let libLock = (dir: string, name: string): string => join(join(dir, "lib"), name)
41
+
42
+ // True when the named lock file exists and holds a live pid.
43
+ let lockHasLivePid = (lock: string): bool =>
44
+ if _existsSync(lock) {
45
+ switch Int.fromString(String.trim(_readFileSync(lock, "utf8"))) {
46
+ | Some(pid) => isAlive(pid)
47
+ | None => false
48
+ }
49
+ } else {
50
+ false
51
+ }
52
+
53
+ // True when an external watcher (either command form) already owns this
54
+ // package's build.
55
+ let hasLiveWatcher = (dir: string): bool =>
56
+ lockHasLivePid(libLock(dir, "watch.lock")) || lockHasLivePid(libLock(dir, "rescript.lock"))
@@ -0,0 +1,59 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
5
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
6
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
9
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
10
+
11
+ let _errCode = ((e) => (e && typeof e.code === "string") ? e.code : undefined);
12
+
13
+ function isAlive(pid) {
14
+ try {
15
+ process.kill(pid, 0);
16
+ return true;
17
+ } catch (raw_exn) {
18
+ let exn = Primitive_exceptions.internalToException(raw_exn);
19
+ let e = Stdlib_JsExn.fromException(exn);
20
+ if (e !== undefined) {
21
+ return Primitive_object.equal(_errCode(Primitive_option.valFromOption(e)), "EPERM");
22
+ } else {
23
+ return false;
24
+ }
25
+ }
26
+ }
27
+
28
+ function libLock(dir, name) {
29
+ return Nodepath.join(Nodepath.join(dir, "lib"), name);
30
+ }
31
+
32
+ function lockHasLivePid(lock) {
33
+ if (!Nodefs.existsSync(lock)) {
34
+ return false;
35
+ }
36
+ let pid = Stdlib_Int.fromString(Nodefs.readFileSync(lock, "utf8").trim(), undefined);
37
+ if (pid !== undefined) {
38
+ return isAlive(pid);
39
+ } else {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function hasLiveWatcher(dir) {
45
+ if (lockHasLivePid(libLock(dir, "watch.lock"))) {
46
+ return true;
47
+ } else {
48
+ return lockHasLivePid(libLock(dir, "rescript.lock"));
49
+ }
50
+ }
51
+
52
+ export {
53
+ _errCode,
54
+ isAlive,
55
+ libLock,
56
+ lockHasLivePid,
57
+ hasLiveWatcher,
58
+ }
59
+ /* node:fs Not a pure module */
package/src/Worker.res ADDED
@@ -0,0 +1,24 @@
1
+ // Minimal `node:worker_threads` binding for gwt watch re-runs.
2
+ //
3
+ // Node's ESM module registry is keyed by URL and never evicts, so re-importing a
4
+ // recompiled test file in the same process replays tests that close over the
5
+ // *old* instances of their statically-imported implementation modules (the
6
+ // Loader can only cache-bust the test file's own URL, not its transitive
7
+ // imports). Running each watch re-run in a short-lived Worker gives it a fresh
8
+ // registry — the recompiled modules are re-imported — and reclaims the whole
9
+ // imported graph on worker exit (fixing the monotonic-buster memory leak).
10
+
11
+ type t
12
+ type workerOptions<'a> = {workerData: 'a}
13
+ type url
14
+
15
+ @new @module("node:worker_threads")
16
+ external make: (url, workerOptions<'a>) => t = "Worker"
17
+
18
+ // A worker's stdout is piped to the parent's process.stdout by default, so the
19
+ // NDJSON the formatters write inside the worker reaches the client unchanged.
20
+ @send external on: (t, string, 'cb) => unit = "on"
21
+ @send external terminate: t => promise<int> = "terminate"
22
+
23
+ // The compiled worker entry lives next to this module in the in-source build.
24
+ let runWorkerUrl: url = %raw(`new URL("./RunWorker.res.mjs", import.meta.url)`)
@@ -0,0 +1,9 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ let runWorkerUrl = (new URL("./RunWorker.res.mjs", import.meta.url));
5
+
6
+ export {
7
+ runWorkerUrl,
8
+ }
9
+ /* runWorkerUrl Not a pure module */
@@ -0,0 +1,71 @@
1
+ // Worked example for AutomationSlice_GWT — a "ship order" automation:
2
+ // OrderPlaced ⇒ TODO, ShipmentCreated resolves the TODO, process emits
3
+ // CreateShipment. Covers the unit combinators (collect / resolve / process)
4
+ // and the scenario combinators (sweep / andThenEvents).
5
+
6
+ @@reventless.gwt
7
+
8
+ module ShipOrderSlice = {
9
+ let name = "ShipOrder"
10
+
11
+ @schema
12
+ type consumedEvent =
13
+ | OrderPlaced({orderId: string, shippingAddress: string})
14
+ | ShipmentCreated({orderId: string})
15
+
16
+ @schema
17
+ type todoItem = {orderId: string, shippingAddress: string}
18
+
19
+ @schema
20
+ type command = CreateShipment({orderId: string, address: string})
21
+
22
+ let collect = event =>
23
+ switch event {
24
+ | OrderPlaced({orderId, shippingAddress}) => [(orderId, {orderId, shippingAddress})]
25
+ | _ => []
26
+ }
27
+
28
+ let resolve = event =>
29
+ switch event {
30
+ | ShipmentCreated({orderId}) => Some(orderId)
31
+ | _ => None
32
+ }
33
+
34
+ let process = (_id, item) =>
35
+ Some((item.orderId, CreateShipment({orderId: item.orderId, address: item.shippingAddress})))
36
+ }
37
+
38
+ describe("ShipOrder AutomationSlice", () => {
39
+ test("collect: OrderPlaced creates a pending TODO", () =>
40
+ givenEvent(OrderPlaced({orderId: "o1", shippingAddress: "1 Main St"}))
41
+ ->whenCollect
42
+ ->thenTodos([("o1", {orderId: "o1", shippingAddress: "1 Main St"})])
43
+ )
44
+
45
+ test("resolve: ShipmentCreated marks the TODO done", () =>
46
+ givenEvent(ShipmentCreated({orderId: "o1"}))
47
+ ->whenResolve
48
+ ->thenResolved(Some("o1"))
49
+ )
50
+
51
+ test("process: pending TODO emits CreateShipment", () =>
52
+ givenTodo("o1", {orderId: "o1", shippingAddress: "1 Main St"})
53
+ ->whenProcess
54
+ ->thenCommand("o1", CreateShipment({orderId: "o1", address: "1 Main St"}))
55
+ )
56
+
57
+ test("sweep: events → commands, andThenEvents drains todos", () => {
58
+ let s =
59
+ givenEvents([OrderPlaced({orderId: "o1", shippingAddress: "1 Main St"})])
60
+ ->whenSweep
61
+ let commandsOk =
62
+ thenCommands(s, [("o1", CreateShipment({orderId: "o1", address: "1 Main St"}))])
63
+ let drained = andThenEvents(s, [ShipmentCreated({orderId: "o1"})])
64
+ let todosOk = thenScenarioTodos(drained, [])
65
+ switch (commandsOk, todosOk) {
66
+ | (Ok(), Ok()) => Outcome.pass
67
+ | (Error(_) as e, _) => e
68
+ | (_, Error(_) as e) => e
69
+ }
70
+ })
71
+ })