@reventlessdev/reventless-gwt 1.0.0-alpha.74
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 +698 -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 +603 -0
- package/src/Behavior_GWT.res.mjs +426 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +137 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +31 -0
- package/src/Cancellation.res.mjs +45 -0
- package/src/ChildProcess.res +84 -0
- package/src/ChildProcess.res.mjs +92 -0
- package/src/Cli.res +671 -0
- package/src/Cli.res.mjs +754 -0
- package/src/Collector.res +201 -0
- package/src/Collector.res.mjs +215 -0
- package/src/ComponentMeta.res +104 -0
- package/src/ComponentMeta.res.mjs +120 -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 +103 -0
- package/src/DomainDeadCode.res +93 -0
- package/src/DomainDeadCode.res.mjs +69 -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 +36 -0
- package/src/Filter.res.mjs +40 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +138 -0
- package/src/FormatterHuman.res.mjs +122 -0
- package/src/FormatterJson.res +257 -0
- package/src/FormatterJson.res.mjs +320 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +139 -0
- package/src/FormatterTap.res.mjs +149 -0
- package/src/FormatterVsCode.res +362 -0
- package/src/FormatterVsCode.res.mjs +564 -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 +135 -0
- package/src/LocalHost.res.mjs +127 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -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 +161 -0
- package/src/Outcome.res.mjs +140 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +160 -0
- package/src/PlatformRunner.res.mjs +155 -0
- package/src/PlatformScan.res +138 -0
- package/src/PlatformScan.res.mjs +131 -0
- package/src/ProcessManager.res +91 -0
- package/src/ProcessManager.res.mjs +80 -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/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 +53 -0
- package/src/StubRuntime.res.mjs +54 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +92 -0
- package/src/Watch.res.mjs +83 -0
- package/src/WatcherProbe.res +48 -0
- package/src/WatcherProbe.res.mjs +46 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +94 -0
- package/tests/BuildClassifierTest.res.mjs +132 -0
- package/tests/ComponentMetaTest.res +52 -0
- package/tests/ComponentMetaTest.res.mjs +57 -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 +116 -0
- package/tests/DomainDeadCodeTest.res.mjs +155 -0
- package/tests/DomainGraphTest.res +365 -0
- package/tests/DomainGraphTest.res.mjs +413 -0
- package/tests/FlowAggregateGwtTest.res +169 -0
- package/tests/FlowAggregateGwtTest.res.mjs +269 -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 +432 -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 +98 -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 +348 -0
- package/tests/MappingGwtTest.res.mjs +592 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +138 -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 +78 -0
- package/tests/WatchDebounceTest.res.mjs +97 -0
package/src/Cli.res
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
// Entry point for the `reventless-gwt` CLI. Orchestrates discovery, the
|
|
2
|
+
// module-level Collector, each formatter, and the watch / cancellation
|
|
3
|
+
// plumbing. Parses a minimal argv layer — no optparse library, no colour
|
|
4
|
+
// auto-detection — kept deliberately thin so the formatters own all
|
|
5
|
+
// presentational logic.
|
|
6
|
+
|
|
7
|
+
let toolVersion = "0.1.0"
|
|
8
|
+
|
|
9
|
+
type format = Human | Json | Tap | Junit | VsCode
|
|
10
|
+
|
|
11
|
+
type subcommand = Run | Discover | Watch | Platform
|
|
12
|
+
|
|
13
|
+
type options = {
|
|
14
|
+
subcommand: subcommand,
|
|
15
|
+
format: format,
|
|
16
|
+
stream: bool,
|
|
17
|
+
watch: bool,
|
|
18
|
+
filters: array<string>,
|
|
19
|
+
schemaVersion: option<string>,
|
|
20
|
+
roots: array<string>,
|
|
21
|
+
// `platform` only: the REVENTLESS_LOCAL_BACKEND value passed to the spawned
|
|
22
|
+
// platform child ("memory" default, or "sqlite:<path>[?reset]").
|
|
23
|
+
backend: string,
|
|
24
|
+
// `platform` only: pin the spawned platform to the fixed default ports
|
|
25
|
+
// (4000/4001/3001/3002) instead of allocating ephemeral ones. The VS Code
|
|
26
|
+
// extension passes this when it is also launching the host-shell UI, whose
|
|
27
|
+
// Vite proxy hardcodes localhost:4000/4001.
|
|
28
|
+
uiPorts: bool,
|
|
29
|
+
// `platform --list` only: enumerate the launchable platform packages (one
|
|
30
|
+
// NDJSON `{name, dir}` line each) and exit, rather than spawning one. Used by
|
|
31
|
+
// the VS Code extension to populate its active-app picker.
|
|
32
|
+
listPlatforms: bool,
|
|
33
|
+
toolVersion: string,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@val external argv: array<string> = "process.argv"
|
|
37
|
+
|
|
38
|
+
@val external now: unit => float = "performance.now"
|
|
39
|
+
|
|
40
|
+
let dateNowIso: unit => string = %raw(`() => new Date().toISOString()`)
|
|
41
|
+
|
|
42
|
+
let parseFormat = (s: string) =>
|
|
43
|
+
switch s {
|
|
44
|
+
| "human" => Ok(Human)
|
|
45
|
+
| "json" => Ok(Json)
|
|
46
|
+
| "tap" => Ok(Tap)
|
|
47
|
+
| "junit" => Ok(Junit)
|
|
48
|
+
| "vscode" => Ok(VsCode)
|
|
49
|
+
| other => Error("Unknown --format value: " ++ other)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// With no explicit path argument, scan the whole working-directory subtree.
|
|
53
|
+
// `Discovery.walk` already prunes `node_modules`/`lib`/`.git`/`dist`/`.history`,
|
|
54
|
+
// so pointing at the cwd auto-discovers every plugin's GWT tests in a
|
|
55
|
+
// multi-package workspace (e.g. an example root) with no roots configured.
|
|
56
|
+
let defaultRoots = () => ["."]
|
|
57
|
+
|
|
58
|
+
let help = () => `reventless-dev — Reventless dev CLI: GWT runner, domain graph, components, platform runner
|
|
59
|
+
|
|
60
|
+
USAGE:
|
|
61
|
+
reventless-dev run [--format=<fmt>] [--filter=<id>] [--stream] [--watch] [path...]
|
|
62
|
+
reventless-dev discover [--format=vscode] [path...]
|
|
63
|
+
reventless-dev watch [--format=<fmt>] [--filter=<id>] [path...]
|
|
64
|
+
reventless-dev platform [--format=vscode] [--backend=<b>] [--ui-ports] [path...]
|
|
65
|
+
reventless-dev platform --list [path...]
|
|
66
|
+
|
|
67
|
+
FORMATS:
|
|
68
|
+
human ANSI-coloured terminal output (default)
|
|
69
|
+
json Structured JSON envelope (--stream for NDJSON)
|
|
70
|
+
tap TAP 14 with YAML diagnostics
|
|
71
|
+
junit JUnit XML (single batch)
|
|
72
|
+
vscode NDJSON event stream for the VS Code Testing API
|
|
73
|
+
|
|
74
|
+
FLAGS:
|
|
75
|
+
--filter <id> Restrict execution to tests whose id contains <id>.
|
|
76
|
+
Repeatable.
|
|
77
|
+
--stream NDJSON streaming (json/vscode)
|
|
78
|
+
--watch Re-run on file change
|
|
79
|
+
--schema-version <v> Pin a JSON schema version for stable AI prompts
|
|
80
|
+
--backend <b> platform: storage backend for the spawned local platform
|
|
81
|
+
("memory" default, or "sqlite:<path>[?reset]")
|
|
82
|
+
--ui-ports platform: pin the platform to the fixed default ports
|
|
83
|
+
(4000/4001/3001/3002) instead of ephemeral ones, so the
|
|
84
|
+
host-shell UI's Vite proxy can reach it
|
|
85
|
+
--list platform: list launchable platform packages as NDJSON
|
|
86
|
+
({name, dir} per line) and exit
|
|
87
|
+
--help Show this help and exit
|
|
88
|
+
|
|
89
|
+
Exit code is 1 if any test failed, 0 otherwise.
|
|
90
|
+
`
|
|
91
|
+
|
|
92
|
+
let parseArgv = (argv: array<string>): result<options, string> => {
|
|
93
|
+
let slice = argv->Array.slice(~start=2, ~end=argv->Array.length)
|
|
94
|
+
let subcommand = ref(Run)
|
|
95
|
+
let format = ref(Human)
|
|
96
|
+
let stream = ref(false)
|
|
97
|
+
let watch = ref(false)
|
|
98
|
+
let filters = ref([])
|
|
99
|
+
let schemaVersion: ref<option<string>> = ref(None)
|
|
100
|
+
let roots = ref([])
|
|
101
|
+
let backend = ref("memory")
|
|
102
|
+
let uiPorts = ref(false)
|
|
103
|
+
let listPlatforms = ref(false)
|
|
104
|
+
let error = ref(None)
|
|
105
|
+
let showHelp = ref(false)
|
|
106
|
+
let i = ref(0)
|
|
107
|
+
let len = slice->Array.length
|
|
108
|
+
if len > 0 {
|
|
109
|
+
let first = slice->Array.getUnsafe(0)
|
|
110
|
+
switch first {
|
|
111
|
+
| "run" => {
|
|
112
|
+
subcommand := Run
|
|
113
|
+
i := 1
|
|
114
|
+
}
|
|
115
|
+
| "discover" => {
|
|
116
|
+
subcommand := Discover
|
|
117
|
+
format := VsCode
|
|
118
|
+
i := 1
|
|
119
|
+
}
|
|
120
|
+
| "watch" => {
|
|
121
|
+
subcommand := Watch
|
|
122
|
+
watch := true
|
|
123
|
+
i := 1
|
|
124
|
+
}
|
|
125
|
+
| "platform" => {
|
|
126
|
+
subcommand := Platform
|
|
127
|
+
i := 1
|
|
128
|
+
}
|
|
129
|
+
| "--help" | "-h" => showHelp := true
|
|
130
|
+
| _ => ()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
while i.contents < len && error.contents == None && !showHelp.contents {
|
|
134
|
+
let arg = slice->Array.getUnsafe(i.contents)
|
|
135
|
+
if arg == "--help" || arg == "-h" {
|
|
136
|
+
showHelp := true
|
|
137
|
+
} else if String.startsWith(arg, "--format=") {
|
|
138
|
+
let v = String.slice(arg, ~start=9, ~end=String.length(arg))
|
|
139
|
+
switch parseFormat(v) {
|
|
140
|
+
| Ok(f) => format := f
|
|
141
|
+
| Error(e) => error := Some(e)
|
|
142
|
+
}
|
|
143
|
+
} else if arg == "--format" && i.contents + 1 < len {
|
|
144
|
+
let v = slice->Array.getUnsafe(i.contents + 1)
|
|
145
|
+
switch parseFormat(v) {
|
|
146
|
+
| Ok(f) => format := f
|
|
147
|
+
| Error(e) => error := Some(e)
|
|
148
|
+
}
|
|
149
|
+
i := i.contents + 1
|
|
150
|
+
} else if String.startsWith(arg, "--filter=") {
|
|
151
|
+
let v = String.slice(arg, ~start=9, ~end=String.length(arg))
|
|
152
|
+
filters := Array.concat(filters.contents, [v])
|
|
153
|
+
} else if arg == "--filter" && i.contents + 1 < len {
|
|
154
|
+
let v = slice->Array.getUnsafe(i.contents + 1)
|
|
155
|
+
filters := Array.concat(filters.contents, [v])
|
|
156
|
+
i := i.contents + 1
|
|
157
|
+
} else if arg == "--stream" {
|
|
158
|
+
stream := true
|
|
159
|
+
} else if arg == "--watch" {
|
|
160
|
+
watch := true
|
|
161
|
+
} else if String.startsWith(arg, "--schema-version=") {
|
|
162
|
+
let v = String.slice(arg, ~start=17, ~end=String.length(arg))
|
|
163
|
+
schemaVersion := Some(v)
|
|
164
|
+
} else if String.startsWith(arg, "--backend=") {
|
|
165
|
+
backend := String.slice(arg, ~start=10, ~end=String.length(arg))
|
|
166
|
+
} else if arg == "--backend" && i.contents + 1 < len {
|
|
167
|
+
backend := slice->Array.getUnsafe(i.contents + 1)
|
|
168
|
+
i := i.contents + 1
|
|
169
|
+
} else if arg == "--ui-ports" {
|
|
170
|
+
uiPorts := true
|
|
171
|
+
} else if arg == "--list" {
|
|
172
|
+
listPlatforms := true
|
|
173
|
+
} else if String.startsWith(arg, "--") {
|
|
174
|
+
error := Some("Unknown flag: " ++ arg)
|
|
175
|
+
} else {
|
|
176
|
+
roots := Array.concat(roots.contents, [arg])
|
|
177
|
+
}
|
|
178
|
+
i := i.contents + 1
|
|
179
|
+
}
|
|
180
|
+
if showHelp.contents {
|
|
181
|
+
Error(help())
|
|
182
|
+
} else {
|
|
183
|
+
switch error.contents {
|
|
184
|
+
| Some(e) => Error(e ++ "\n\n" ++ help())
|
|
185
|
+
| None =>
|
|
186
|
+
Ok({
|
|
187
|
+
subcommand: subcommand.contents,
|
|
188
|
+
format: format.contents,
|
|
189
|
+
stream: stream.contents,
|
|
190
|
+
watch: watch.contents,
|
|
191
|
+
filters: filters.contents,
|
|
192
|
+
schemaVersion: schemaVersion.contents,
|
|
193
|
+
roots: roots.contents->Array.length == 0 ? defaultRoots() : roots.contents,
|
|
194
|
+
backend: backend.contents,
|
|
195
|
+
uiPorts: uiPorts.contents,
|
|
196
|
+
listPlatforms: listPlatforms.contents,
|
|
197
|
+
toolVersion,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Apply --filter: pass if id contains every filter substring, OR if no filters.
|
|
204
|
+
let passesFilter = (id: string, filters: array<string>) =>
|
|
205
|
+
filters->Array.length == 0 ||
|
|
206
|
+
filters->Array.some(f => id->String.includes(f))
|
|
207
|
+
|
|
208
|
+
// Extract a human-readable message from any thrown value. JS Errors carry a
|
|
209
|
+
// `.message`; ReScript exceptions are tagged objects (`RE_EXN_ID` + an optional
|
|
210
|
+
// string payload), e.g. `failwith("…")` raises `Failure("…")`. Without this the
|
|
211
|
+
// catch block below collapsed every ReScript exception to "unknown error",
|
|
212
|
+
// hiding messages like a slice's `failwith("not implemented: …")`.
|
|
213
|
+
let exnMessage: exn => string = %raw(`function(e) {
|
|
214
|
+
if (e == null) return "unknown error"
|
|
215
|
+
if (typeof e === "string") return e
|
|
216
|
+
if (typeof e.message === "string" && e.message.length) return e.message
|
|
217
|
+
if (typeof e.RE_EXN_ID === "string") {
|
|
218
|
+
var id = e.RE_EXN_ID
|
|
219
|
+
var tag = id.lastIndexOf(".") >= 0 ? id.slice(id.lastIndexOf(".") + 1) : id
|
|
220
|
+
return (typeof e._1 === "string" && e._1.length) ? e._1 : tag
|
|
221
|
+
}
|
|
222
|
+
return "unknown error"
|
|
223
|
+
}`)
|
|
224
|
+
|
|
225
|
+
// Execute a single entry, catching exceptions and thrown values so a
|
|
226
|
+
// misbehaved slice doesn't abort the rest of the suite.
|
|
227
|
+
let runEntry = async (entry: Collector.entry): RunnerTypes.testResult => {
|
|
228
|
+
let start = now()
|
|
229
|
+
let status = ref(RunnerTypes.Pass)
|
|
230
|
+
let mismatch: ref<option<Outcome.mismatch>> = ref(None)
|
|
231
|
+
switch entry.status {
|
|
232
|
+
| Collector.Skipped => status := Skip
|
|
233
|
+
| _ =>
|
|
234
|
+
try {
|
|
235
|
+
let outcome = await entry.body()
|
|
236
|
+
switch outcome {
|
|
237
|
+
| Ok() => status := Pass
|
|
238
|
+
| Error(m) => {
|
|
239
|
+
status := Fail
|
|
240
|
+
mismatch := Some(m)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
| exn => {
|
|
245
|
+
status := Fail
|
|
246
|
+
let err = exnMessage(exn)
|
|
247
|
+
let stack: string = %raw(`(e => (e && e.stack) || "")`)(exn)
|
|
248
|
+
mismatch := Some(Outcome.Throw({error: err, stack: stack}))
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
let durationMs = now() -. start
|
|
253
|
+
{
|
|
254
|
+
id: entry.id,
|
|
255
|
+
name: entry.name,
|
|
256
|
+
describePath: entry.describePath,
|
|
257
|
+
status: status.contents,
|
|
258
|
+
durationMs,
|
|
259
|
+
location: entry.location,
|
|
260
|
+
slice: entry.slice,
|
|
261
|
+
mismatch: mismatch.contents,
|
|
262
|
+
skipReason: entry.status == Collector.Skipped ? Some("skipped at source") : None,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let loadAndCollect = async (path: string): array<Collector.entry> => {
|
|
267
|
+
Collector.activate()
|
|
268
|
+
Collector.setCurrentFile(path)
|
|
269
|
+
try {
|
|
270
|
+
await Loader.loadFile(path)
|
|
271
|
+
} catch {
|
|
272
|
+
| exn =>
|
|
273
|
+
let msg =
|
|
274
|
+
exn
|
|
275
|
+
->JsExn.fromException
|
|
276
|
+
->Option.flatMap(JsExn.message)
|
|
277
|
+
->Option.getOr("import failed")
|
|
278
|
+
Collector.push(
|
|
279
|
+
`<file-load-error>`,
|
|
280
|
+
() =>
|
|
281
|
+
Promise.resolve(
|
|
282
|
+
Outcome.fail(
|
|
283
|
+
Throw({error: "Failed to load " ++ path ++ ": " ++ msg, stack: ""}),
|
|
284
|
+
),
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
let entries = Collector.drain()
|
|
289
|
+
Collector.deactivate()
|
|
290
|
+
entries
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let runFiles = async (
|
|
294
|
+
opts: options,
|
|
295
|
+
paths: array<string>,
|
|
296
|
+
~onFileFinished: option<RunnerTypes.fileResult => unit>=?,
|
|
297
|
+
~onTestStart: option<(string, Collector.entry) => unit>=?,
|
|
298
|
+
~onTestFinished: option<(string, RunnerTypes.testResult) => unit>=?,
|
|
299
|
+
): RunnerTypes.runResult => {
|
|
300
|
+
let startedAt = dateNowIso()
|
|
301
|
+
let startTime = now()
|
|
302
|
+
let files: array<RunnerTypes.fileResult> = []
|
|
303
|
+
for i in 0 to paths->Array.length - 1 {
|
|
304
|
+
let path = paths->Array.getUnsafe(i)
|
|
305
|
+
if Cancellation.isCancelled() {
|
|
306
|
+
()
|
|
307
|
+
} else {
|
|
308
|
+
let entries = await loadAndCollect(path)
|
|
309
|
+
let onlyActive = Collector.hasOnly.contents
|
|
310
|
+
let selected = entries->Array.filter(e => {
|
|
311
|
+
let keepByOnly = onlyActive ? e.status == Only : true
|
|
312
|
+
// Match filters against both the bare entry id (`describe::name`) and
|
|
313
|
+
// the fully-qualified id the VS Code extension uses
|
|
314
|
+
// (`<absFile>::<describe>::<name>`). Without the qualified form the
|
|
315
|
+
// extension's `--filter=<testId>` never matches — see continuous-run
|
|
316
|
+
// stale-failure fix.
|
|
317
|
+
let qualifiedId = path ++ "::" ++ e.id
|
|
318
|
+
let keepByFilter =
|
|
319
|
+
passesFilter(e.id, opts.filters) || passesFilter(qualifiedId, opts.filters)
|
|
320
|
+
keepByOnly && keepByFilter
|
|
321
|
+
})
|
|
322
|
+
let tests: array<RunnerTypes.testResult> = []
|
|
323
|
+
for j in 0 to selected->Array.length - 1 {
|
|
324
|
+
if Cancellation.isCancelled() {
|
|
325
|
+
let e = selected->Array.getUnsafe(j)
|
|
326
|
+
let r: RunnerTypes.testResult = {
|
|
327
|
+
id: e.id,
|
|
328
|
+
name: e.name,
|
|
329
|
+
describePath: e.describePath,
|
|
330
|
+
status: Skip,
|
|
331
|
+
durationMs: 0.0,
|
|
332
|
+
location: e.location,
|
|
333
|
+
slice: e.slice,
|
|
334
|
+
mismatch: None,
|
|
335
|
+
skipReason: Some("cancelled"),
|
|
336
|
+
}
|
|
337
|
+
tests->Array.push(r)
|
|
338
|
+
switch onTestFinished {
|
|
339
|
+
| Some(cb) => cb(path, r)
|
|
340
|
+
| None => ()
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
let entry = selected->Array.getUnsafe(j)
|
|
344
|
+
switch onTestStart {
|
|
345
|
+
| Some(cb) => cb(path, entry)
|
|
346
|
+
| None => ()
|
|
347
|
+
}
|
|
348
|
+
let r = await runEntry(entry)
|
|
349
|
+
tests->Array.push(r)
|
|
350
|
+
switch onTestFinished {
|
|
351
|
+
| Some(cb) => cb(path, r)
|
|
352
|
+
| None => ()
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let fr: RunnerTypes.fileResult = {path, tests}
|
|
357
|
+
files->Array.push(fr)
|
|
358
|
+
switch onFileFinished {
|
|
359
|
+
| Some(cb) => cb(fr)
|
|
360
|
+
| None => ()
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
let durationMs = now() -. startTime
|
|
365
|
+
let summary = RunnerTypes.summaryOf(files, durationMs, startedAt)
|
|
366
|
+
{summary, files}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let emitResult = (opts: options, r: RunnerTypes.runResult) =>
|
|
370
|
+
switch opts.format {
|
|
371
|
+
| Human => FormatterHuman.emit(r)
|
|
372
|
+
| Json =>
|
|
373
|
+
if opts.stream {
|
|
374
|
+
FormatterJson.streamRunEnd(r)
|
|
375
|
+
} else {
|
|
376
|
+
FormatterJson.emit(r, ~toolVersion=opts.toolVersion)
|
|
377
|
+
}
|
|
378
|
+
| Tap => FormatterTap.emit(r)
|
|
379
|
+
| Junit => FormatterJunit.emit(r)
|
|
380
|
+
| VsCode => FormatterVsCode.runEnd(r.summary)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let runOnce = async (opts: options): int => {
|
|
384
|
+
let paths = await Discovery.discover(opts.roots)
|
|
385
|
+
if opts.format == Json && opts.stream {
|
|
386
|
+
FormatterJson.streamRunStart(~toolVersion=opts.toolVersion, ~startedAt=dateNowIso())
|
|
387
|
+
}
|
|
388
|
+
let onFileFinished: option<RunnerTypes.fileResult => unit> = switch (
|
|
389
|
+
opts.format,
|
|
390
|
+
opts.stream,
|
|
391
|
+
) {
|
|
392
|
+
| (Json, true) =>
|
|
393
|
+
Some(
|
|
394
|
+
(f: RunnerTypes.fileResult) => {
|
|
395
|
+
f.tests->Array.forEach(FormatterJson.streamTest)
|
|
396
|
+
FormatterJson.streamFileFinished(f)
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
| _ => None
|
|
400
|
+
}
|
|
401
|
+
// VS Code emits per-test events (testStart / testPass / testFail / testSkip)
|
|
402
|
+
// so the extension can update the UI in real time. IDs are prefixed with the
|
|
403
|
+
// absolute file path to match the ids emitted by `discover`.
|
|
404
|
+
let onTestStart: option<(string, Collector.entry) => unit> =
|
|
405
|
+
opts.format == VsCode
|
|
406
|
+
? Some((path, e) => FormatterVsCode.testStart(path ++ "::" ++ e.id))
|
|
407
|
+
: None
|
|
408
|
+
let onTestFinished: option<(string, RunnerTypes.testResult) => unit> =
|
|
409
|
+
opts.format == VsCode
|
|
410
|
+
? Some(
|
|
411
|
+
(path, t) => {
|
|
412
|
+
let id = path ++ "::" ++ t.id
|
|
413
|
+
switch t.status {
|
|
414
|
+
| Pass => FormatterVsCode.testPass(id, t.durationMs)
|
|
415
|
+
| Fail => FormatterVsCode.testFail(t, id)
|
|
416
|
+
| Skip =>
|
|
417
|
+
FormatterVsCode.testSkip(id, t.skipReason->Option.getOr("skipped"))
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
: None
|
|
422
|
+
// For streaming formatters, emit per-file events; emitResult only emits
|
|
423
|
+
// the terminal `runEnd`.
|
|
424
|
+
if opts.format == Json && opts.stream {
|
|
425
|
+
paths->Array.forEach(p => FormatterJson.streamFileStart(p))
|
|
426
|
+
} else if opts.format == VsCode {
|
|
427
|
+
FormatterVsCode.runStart(~total=0, ~filter=opts.filters)
|
|
428
|
+
}
|
|
429
|
+
let result = await runFiles(opts, paths, ~onFileFinished?, ~onTestStart?, ~onTestFinished?)
|
|
430
|
+
emitResult(opts, result)
|
|
431
|
+
result.summary.failed > 0 ? 1 : 0
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Reflects each plugin's pluginStructure via the local host (a single cold load) and
|
|
435
|
+
// emits the domain-analysis events: `deadCode` (orphan events) and `graph` (the
|
|
436
|
+
// Event-Modeling node/edge model). Skips silently when there are no plugin packages
|
|
437
|
+
// or the local platform isn't resolvable, and swallows any load failure so the
|
|
438
|
+
// discovery stream is never compromised.
|
|
439
|
+
let emitDomainAnalysis = async (pkgs: array<PackageScan.pkg>): unit =>
|
|
440
|
+
switch LocalHost.discover(~packageDirs=pkgs->Array.map(p => p.dir)) {
|
|
441
|
+
| [] => ()
|
|
442
|
+
| plugins =>
|
|
443
|
+
switch LocalHost.resolveLocalPlatform(~fromPackageDir=(plugins->Array.getUnsafe(0)).packageDir) {
|
|
444
|
+
| None => ()
|
|
445
|
+
| Some(platformModulePath) =>
|
|
446
|
+
try {
|
|
447
|
+
let loaded = await LocalHost.loadGraph(~platformModulePath, ~plugins)
|
|
448
|
+
FormatterVsCode.deadCode(DomainDeadCode.analyze(~structures=loaded.structures))
|
|
449
|
+
FormatterVsCode.graph(DomainGraph.build(~structures=loaded.structures))
|
|
450
|
+
// Field schemas for command/event/read-side state (Phase 6.3): the same
|
|
451
|
+
// per-plugin entry the Platform_ComponentDefinitions GraphQL query returns.
|
|
452
|
+
FormatterVsCode.definitions(
|
|
453
|
+
loaded.structures->Array.map(((pluginId, s)) =>
|
|
454
|
+
ReventlessCore.Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId, s)
|
|
455
|
+
),
|
|
456
|
+
)
|
|
457
|
+
} catch {
|
|
458
|
+
| _ => ()
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Emits the full VS Code discovery stream (tree items + `packages`) for a set
|
|
464
|
+
// of already-discovered test files. Shared by `discover` and `watch`.
|
|
465
|
+
let emitDiscovery = async (paths: array<string>): unit => {
|
|
466
|
+
FormatterVsCode.discoverStart()
|
|
467
|
+
let fileTests: array<(string, array<RunnerTypes.testResult>)> = []
|
|
468
|
+
for i in 0 to paths->Array.length - 1 {
|
|
469
|
+
let path = paths->Array.getUnsafe(i)
|
|
470
|
+
let entries = await loadAndCollect(path)
|
|
471
|
+
let tests: array<RunnerTypes.testResult> =
|
|
472
|
+
entries->Array.map(e => {
|
|
473
|
+
RunnerTypes.id: e.id,
|
|
474
|
+
name: e.name,
|
|
475
|
+
describePath: e.describePath,
|
|
476
|
+
status: Skip,
|
|
477
|
+
durationMs: 0.0,
|
|
478
|
+
location: e.location,
|
|
479
|
+
slice: e.slice,
|
|
480
|
+
mismatch: None,
|
|
481
|
+
skipReason: None,
|
|
482
|
+
})
|
|
483
|
+
fileTests->Array.push((path, tests))
|
|
484
|
+
}
|
|
485
|
+
FormatterVsCode.emitDiscoveryItems(fileTests)
|
|
486
|
+
let total = fileTests->Array.reduce(0, (a, (_, ts)) => a + ts->Array.length)
|
|
487
|
+
let pkgs = PackageScan.scan(paths)
|
|
488
|
+
FormatterVsCode.packages(pkgs)
|
|
489
|
+
// Full component inventory (incl. untested components) from each package's src/.
|
|
490
|
+
let comps = await ComponentScan.scan(pkgs->Array.map(p => p.dir))
|
|
491
|
+
FormatterVsCode.components(comps)
|
|
492
|
+
FormatterVsCode.discoverEnd(total)
|
|
493
|
+
// Domain analysis (dead-code + graph): emitted after the core stream so the tree
|
|
494
|
+
// shows first. One cold load of the local platform reflects pluginStructure —
|
|
495
|
+
// best-effort; never breaks discovery.
|
|
496
|
+
await emitDomainAnalysis(pkgs)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let runDiscover = async (opts: options): int => {
|
|
500
|
+
let paths = await Discovery.discover(opts.roots)
|
|
501
|
+
await emitDiscovery(paths)
|
|
502
|
+
0
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Spawns (or adopts) a `rescript build -w` per package owning tests, wiring its
|
|
506
|
+
// output to build-status events. Only for the VS Code client — a human `watch`
|
|
507
|
+
// in a terminal keeps the lean re-run-only loop.
|
|
508
|
+
let startBuildWatchers = (paths: array<string>): unit => {
|
|
509
|
+
Cancellation.onCancel(ProcessManager.killAll)
|
|
510
|
+
PackageScan.scan(paths)->Array.forEach(pkg =>
|
|
511
|
+
if WatcherProbe.hasLiveWatcher(pkg.dir) {
|
|
512
|
+
// A developer's own watcher already covers this package — defer to it.
|
|
513
|
+
FormatterVsCode.buildExternal(pkg.dir)
|
|
514
|
+
} else {
|
|
515
|
+
let feed = BuildClassifier.make({
|
|
516
|
+
onStart: () => FormatterVsCode.buildStart(pkg.dir),
|
|
517
|
+
onOk: ms => FormatterVsCode.buildOk(pkg.dir, ms),
|
|
518
|
+
onFail: msg => FormatterVsCode.buildFail(pkg.dir, msg),
|
|
519
|
+
})
|
|
520
|
+
ProcessManager.spawnWatcher(pkg, ~onStdout=(_dir, line) => feed(line))
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let runWatch = async (opts: options): int => {
|
|
526
|
+
let roots = opts.roots
|
|
527
|
+
// For the VS Code client, watch mode is the whole engine: emit the test tree
|
|
528
|
+
// and package set, take over the ReScript builds, then run + re-run on change.
|
|
529
|
+
if opts.format == VsCode {
|
|
530
|
+
let paths = await Discovery.discover(roots)
|
|
531
|
+
await emitDiscovery(paths)
|
|
532
|
+
startBuildWatchers(paths)
|
|
533
|
+
}
|
|
534
|
+
let runInProgress = ref(false)
|
|
535
|
+
let rerunPending = ref(false)
|
|
536
|
+
let run = async () => {
|
|
537
|
+
if runInProgress.contents {
|
|
538
|
+
rerunPending := true
|
|
539
|
+
} else {
|
|
540
|
+
runInProgress := true
|
|
541
|
+
let _ = await runOnce(opts)
|
|
542
|
+
runInProgress := false
|
|
543
|
+
if rerunPending.contents {
|
|
544
|
+
rerunPending := false
|
|
545
|
+
let _ = await runOnce(opts)
|
|
546
|
+
()
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
let _ = await run()
|
|
551
|
+
// A *relocation* (a module moved to another directory — a chapter rename /
|
|
552
|
+
// ungroup / move-to-chapter in the extension, or a terminal `mv`) leaves
|
|
553
|
+
// dependents' emitted `.res.mjs` pointing at the old path: incremental
|
|
554
|
+
// compilation never marks them dirty (their source + the moved module's
|
|
555
|
+
// interface hash are unchanged), so the live watcher and a restart both skip
|
|
556
|
+
// them and the stale import surfaces as a `<file-load-error>`. The only
|
|
557
|
+
// reliable repair is a `rescript clean` of the owning package. Detect the
|
|
558
|
+
// structural signal — an `unlink` of a source `.res` (a module left a
|
|
559
|
+
// directory) — and clean-rebuild that package, gating the re-run on build
|
|
560
|
+
// completion. Edits and adds stay on the lean incremental re-run. VS Code
|
|
561
|
+
// only: a human terminal `watch` keeps the re-run-only loop (it doesn't own
|
|
562
|
+
// the builds, so there's nothing to clean-rebuild).
|
|
563
|
+
let structuralRebuild = (path: string): unit =>
|
|
564
|
+
switch PackageScan.findOwning(path) {
|
|
565
|
+
| None => ignore(run())
|
|
566
|
+
| Some(pkg) =>
|
|
567
|
+
let t0 = Date.now()
|
|
568
|
+
FormatterVsCode.buildStart(pkg.dir)
|
|
569
|
+
// Surface a compile error during the rebuild (the clean/build stream is
|
|
570
|
+
// classified for the error marker); the re-run is gated on completion
|
|
571
|
+
// below, not on the classifier, since a one-shot `rescript build` has no
|
|
572
|
+
// "Finished … compilation" terminator.
|
|
573
|
+
let feed = BuildClassifier.make({
|
|
574
|
+
onStart: () => (),
|
|
575
|
+
onOk: _ms => (),
|
|
576
|
+
onFail: msg => FormatterVsCode.buildFail(pkg.dir, msg),
|
|
577
|
+
})
|
|
578
|
+
ProcessManager.cleanRebuild(
|
|
579
|
+
pkg,
|
|
580
|
+
~onStdout=(_dir, line) => feed(line),
|
|
581
|
+
~onDone=() => {
|
|
582
|
+
FormatterVsCode.buildOk(pkg.dir, Date.now() -. t0)
|
|
583
|
+
ignore(run())
|
|
584
|
+
},
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
let _watcher = Watch.start(roots, (event, path) =>
|
|
588
|
+
if opts.format == VsCode && Watch.isStructuralSource(event, path) {
|
|
589
|
+
structuralRebuild(path)
|
|
590
|
+
} else {
|
|
591
|
+
ignore(run())
|
|
592
|
+
}
|
|
593
|
+
)
|
|
594
|
+
// Keep the process alive until cancelled. Awaiting here (rather than letting
|
|
595
|
+
// runWatch return) is essential: the bin wrapper calls `process.exit` on the
|
|
596
|
+
// resolved code, so a returning runWatch would tear down chokidar and the
|
|
597
|
+
// spawned build watchers immediately. Cancellation handlers run in
|
|
598
|
+
// registration order — ProcessManager.killAll first, then this resolve — so
|
|
599
|
+
// the watchers are killed before the process exits.
|
|
600
|
+
await Promise.make((resolve, _reject) => Cancellation.onCancel(() => resolve()))
|
|
601
|
+
0
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Launches an app's reventless-local platform as a managed child with the domain
|
|
605
|
+
// event tap on, streaming lifecycle + events. For the VS Code client the callbacks
|
|
606
|
+
// emit NDJSON; a human `platform` prints readable lines and passes the child's own
|
|
607
|
+
// logs through.
|
|
608
|
+
let runPlatform = async (opts: options): int => {
|
|
609
|
+
let callbacks: PlatformRunner.callbacks = if opts.format == VsCode {
|
|
610
|
+
{
|
|
611
|
+
onStart: (~package, ~dir, ~domainPort, ~platformPort) =>
|
|
612
|
+
FormatterVsCode.platformStart(~package, ~dir, ~domainPort, ~platformPort),
|
|
613
|
+
onReady: (~domainEndpoint) => FormatterVsCode.platformReady(~domainEndpoint),
|
|
614
|
+
onDomainEvent: json => FormatterVsCode.domainEvent(json),
|
|
615
|
+
onLog: line => FormatterVsCode.platformLog(~line),
|
|
616
|
+
onStop: code => FormatterVsCode.platformStop(~code),
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
{
|
|
620
|
+
onStart: (~package, ~dir as _, ~domainPort, ~platformPort as _) =>
|
|
621
|
+
Console.log(`▶ platform ${package} starting (domain http://localhost:${Int.toString(domainPort)})`),
|
|
622
|
+
onReady: (~domainEndpoint) => Console.log(`✓ platform ready — ${domainEndpoint}`),
|
|
623
|
+
onDomainEvent: json => Console.log("· event " ++ JSON.stringify(json)),
|
|
624
|
+
onLog: line => Console.log(line),
|
|
625
|
+
onStop: code =>
|
|
626
|
+
Console.log(`■ platform stopped (code ${code->Option.mapOr("?", c => Int.toString(c))})`),
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
await PlatformRunner.run(
|
|
630
|
+
~roots=opts.roots,
|
|
631
|
+
~backend=opts.backend,
|
|
632
|
+
~fixedPorts=opts.uiPorts,
|
|
633
|
+
~callbacks,
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// `platform --list`: enumerate the launchable platform packages under the roots
|
|
638
|
+
// and print one NDJSON `{name, dir}` object per line. The VS Code extension reads
|
|
639
|
+
// these to populate its active-app picker (app root = the package's parent dir).
|
|
640
|
+
// Plain stdout, no protocol handshake — it's a one-shot query, not a stream.
|
|
641
|
+
let listPlatforms = async (opts: options): int => {
|
|
642
|
+
let pkgs = await PlatformScan.scan(opts.roots)
|
|
643
|
+
pkgs->Array.forEach(pkg => {
|
|
644
|
+
let obj = Dict.fromArray([("name", JSON.String(pkg.name)), ("dir", JSON.String(pkg.dir))])
|
|
645
|
+
Console.log(JSON.stringify(JSON.Object(obj)))
|
|
646
|
+
})
|
|
647
|
+
0
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let main = async (): int => {
|
|
651
|
+
Cancellation.install()
|
|
652
|
+
let args = argv
|
|
653
|
+
switch parseArgv(args) {
|
|
654
|
+
| Error(msg) => {
|
|
655
|
+
Console.error(msg)
|
|
656
|
+
args->Array.some(a => a == "--help" || a == "-h") ? 0 : 2
|
|
657
|
+
}
|
|
658
|
+
| Ok(opts) =>
|
|
659
|
+
// Protocol handshake first, so a version-skewed extension can detect the
|
|
660
|
+
// CLI before interpreting the stream.
|
|
661
|
+
if opts.format == VsCode {
|
|
662
|
+
FormatterVsCode.hello()
|
|
663
|
+
}
|
|
664
|
+
switch opts.subcommand {
|
|
665
|
+
| Run => await runOnce(opts)
|
|
666
|
+
| Discover => await runDiscover(opts)
|
|
667
|
+
| Watch => await runWatch(opts)
|
|
668
|
+
| Platform => opts.listPlatforms ? await listPlatforms(opts) : await runPlatform(opts)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|