@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.
- package/CHANGELOG.md +916 -0
- package/LICENSE +202 -0
- package/bin/reventless-dev.mjs +21 -0
- package/package.json +54 -0
- package/rescript.json +36 -0
- package/src/.gwtignore +10 -0
- package/src/Automation_GWT.res +207 -0
- package/src/Automation_GWT.res.mjs +160 -0
- package/src/Behavior_GWT.res +569 -0
- package/src/Behavior_GWT.res.mjs +523 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +147 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +37 -0
- package/src/Cancellation.res.mjs +46 -0
- package/src/ChildProcess.res +102 -0
- package/src/ChildProcess.res.mjs +107 -0
- package/src/Cli.res +905 -0
- package/src/Cli.res.mjs +952 -0
- package/src/Collector.res +212 -0
- package/src/Collector.res.mjs +218 -0
- package/src/ComponentMeta.res +77 -0
- package/src/ComponentMeta.res.mjs +91 -0
- package/src/ComponentScan.res +103 -0
- package/src/ComponentScan.res.mjs +105 -0
- package/src/Delegate_GWT.res +303 -0
- package/src/Delegate_GWT.res.mjs +372 -0
- package/src/Diff.res +91 -0
- package/src/Diff.res.mjs +98 -0
- package/src/Discovery.res +105 -0
- package/src/Discovery.res.mjs +99 -0
- package/src/DomainDeadCode.res +98 -0
- package/src/DomainDeadCode.res.mjs +72 -0
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/EventMapping_GWT.res +29 -0
- package/src/EventMapping_GWT.res.mjs +44 -0
- package/src/Filter.res +45 -0
- package/src/Filter.res.mjs +47 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +109 -0
- package/src/FormatterHuman.res.mjs +115 -0
- package/src/FormatterJson.res +264 -0
- package/src/FormatterJson.res.mjs +322 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +100 -0
- package/src/FormatterTap.res.mjs +131 -0
- package/src/FormatterVsCode.res +352 -0
- package/src/FormatterVsCode.res.mjs +546 -0
- package/src/Hint.res +119 -0
- package/src/Hint.res.mjs +124 -0
- package/src/InboundTranslation_GWT.res +83 -0
- package/src/InboundTranslation_GWT.res.mjs +94 -0
- package/src/JestBind.res +70 -0
- package/src/JestBind.res.mjs +63 -0
- package/src/Loader.res +24 -0
- package/src/Loader.res.mjs +23 -0
- package/src/LocalHost.res +119 -0
- package/src/LocalHost.res.mjs +99 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -0
- package/src/MultiSourceProjection_GWT.res +316 -0
- package/src/MultiSourceProjection_GWT.res.mjs +354 -0
- package/src/OutboundTranslation_GWT.res +238 -0
- package/src/OutboundTranslation_GWT.res.mjs +186 -0
- package/src/Outcome.res +97 -0
- package/src/Outcome.res.mjs +93 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +181 -0
- package/src/PlatformRunner.res.mjs +176 -0
- package/src/PlatformScan.res +142 -0
- package/src/PlatformScan.res.mjs +132 -0
- package/src/ProcessManager.res +135 -0
- package/src/ProcessManager.res.mjs +119 -0
- package/src/Projection_GWT.res +312 -0
- package/src/Projection_GWT.res.mjs +332 -0
- package/src/Query_GWT.res +379 -0
- package/src/Query_GWT.res.mjs +367 -0
- package/src/RenderRescript.res +125 -0
- package/src/RenderRescript.res.mjs +141 -0
- package/src/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -0
- package/src/RunnerTypes.res +68 -0
- package/src/RunnerTypes.res.mjs +60 -0
- package/src/SideEffect_GWT.res +157 -0
- package/src/SideEffect_GWT.res.mjs +113 -0
- package/src/StateChange/ExternalAddCategorySlice.res +25 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
- package/src/StateChange/WithFixtures.res +22 -0
- package/src/StateChange/WithFixtures.res.mjs +31 -0
- package/src/StateChange/WithFixtures_Behavior.res +17 -0
- package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
- package/src/StateChange/WithManualOpen.res +23 -0
- package/src/StateChange/WithManualOpen.res.mjs +31 -0
- package/src/StateChange/WithManualOpen_Behavior.res +17 -0
- package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
- package/src/StubRuntime.res +54 -0
- package/src/StubRuntime.res.mjs +55 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +108 -0
- package/src/Watch.res.mjs +85 -0
- package/src/WatcherProbe.res +56 -0
- package/src/WatcherProbe.res.mjs +59 -0
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +133 -0
- package/tests/BuildClassifierTest.res.mjs +176 -0
- package/tests/CliRunEntryTest.res +57 -0
- package/tests/CliRunEntryTest.res.mjs +65 -0
- package/tests/CliWatchScopeTest.res +62 -0
- package/tests/CliWatchScopeTest.res.mjs +95 -0
- package/tests/ComponentMetaTest.res +67 -0
- package/tests/ComponentMetaTest.res.mjs +71 -0
- package/tests/DelegateGwtTest.res +487 -0
- package/tests/DelegateGwtTest.res.mjs +891 -0
- package/tests/DiscoveryTest.res +42 -0
- package/tests/DiscoveryTest.res.mjs +43 -0
- package/tests/DomainDeadCodeTest.res +119 -0
- package/tests/DomainDeadCodeTest.res.mjs +158 -0
- package/tests/DomainGraphTest.res +448 -0
- package/tests/DomainGraphTest.res.mjs +505 -0
- package/tests/FlowAggregateGwtTest.res +171 -0
- package/tests/FlowAggregateGwtTest.res.mjs +273 -0
- package/tests/FlowCrossPluginGwtTest.res +340 -0
- package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
- package/tests/FlowGwtTest.res +222 -0
- package/tests/FlowGwtTest.res.mjs +433 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -0
- package/tests/FormatterVsCodeMessageTest.res +41 -0
- package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
- package/tests/InboundTranslationGwtTest.res +34 -0
- package/tests/InboundTranslationGwtTest.res.mjs +99 -0
- package/tests/LocalHostIntegration.res +70 -0
- package/tests/LocalHostIntegration.res.mjs +64 -0
- package/tests/LocalHostTest.res +71 -0
- package/tests/LocalHostTest.res.mjs +63 -0
- package/tests/MappingGwtTest.res +350 -0
- package/tests/MappingGwtTest.res.mjs +596 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
- package/tests/PlatformRunnerTest.res +66 -0
- package/tests/PlatformRunnerTest.res.mjs +85 -0
- package/tests/PlatformScanTest.res +61 -0
- package/tests/PlatformScanTest.res.mjs +56 -0
- package/tests/QueryGwtTest.res +161 -0
- package/tests/QueryGwtTest.res.mjs +314 -0
- package/tests/QueryResolverGwtTest.res +71 -0
- package/tests/QueryResolverGwtTest.res.mjs +136 -0
- package/tests/RunnerUnitTest.res +219 -0
- package/tests/RunnerUnitTest.res.mjs +244 -0
- package/tests/SideEffect/FakeOrderNotification.res +23 -0
- package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
- package/tests/SideEffect/MockEmail.res +18 -0
- package/tests/SideEffect/MockEmail.res.mjs +47 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
- package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
- package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithFixtures_GWT.res +23 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithManualOpen_GWT.res +17 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
- package/tests/StateChangeSliceGwtTest.res +160 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
- package/tests/StateViewSliceGwtTest.res +64 -0
- package/tests/StateViewSliceGwtTest.res.mjs +161 -0
- package/tests/WatchDebounceTest.res +106 -0
- 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,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
|
+
})
|