@reventlessdev/reventless-gwt 1.0.0-alpha.76 → 1.0.0-alpha.78
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 +16 -0
- package/package.json +7 -7
- package/src/Behavior_GWT.res +125 -159
- package/src/Behavior_GWT.res.mjs +156 -59
- package/src/BuildClassifier.res +15 -5
- package/src/BuildClassifier.res.mjs +3 -3
- package/src/Cli.res +157 -29
- package/src/Cli.res.mjs +138 -24
- package/src/Collector.res +2 -2
- package/src/Collector.res.mjs +2 -2
- package/src/ComponentMeta.res +16 -43
- package/src/ComponentMeta.res.mjs +12 -41
- package/src/Discovery.res +19 -19
- package/src/Discovery.res.mjs +5 -9
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/FormatterHuman.res +14 -43
- package/src/FormatterHuman.res.mjs +21 -28
- package/src/FormatterJson.res +12 -5
- package/src/FormatterJson.res.mjs +8 -6
- package/src/FormatterTap.res +13 -52
- package/src/FormatterTap.res.mjs +25 -43
- package/src/FormatterVsCode.res +4 -19
- package/src/FormatterVsCode.res.mjs +4 -23
- package/src/LocalHost.res +11 -27
- package/src/LocalHost.res.mjs +6 -34
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -0
- package/src/Outcome.res +0 -64
- package/src/Outcome.res.mjs +0 -47
- package/src/PlatformScan.res +14 -10
- package/src/PlatformScan.res.mjs +14 -13
- package/tests/BuildClassifierTest.res +39 -0
- package/tests/BuildClassifierTest.res.mjs +44 -0
- package/tests/CliWatchScopeTest.res +62 -0
- package/tests/CliWatchScopeTest.res.mjs +95 -0
- package/tests/ComponentMetaTest.res +15 -0
- package/tests/ComponentMetaTest.res.mjs +14 -0
- package/tests/DomainGraphTest.res +2 -2
- package/tests/FlowAggregateGwtTest.res +2 -0
- package/tests/FlowAggregateGwtTest.res.mjs +4 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -0
- package/tests/MappingGwtTest.res +2 -0
- package/tests/MappingGwtTest.res.mjs +4 -0
package/src/BuildClassifier.res
CHANGED
|
@@ -74,11 +74,21 @@ let make = (~watchdogMs: int=120_000, cb: callbacks): (string => unit) => {
|
|
|
74
74
|
buffer := []
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
77
|
+
// Cap the failure message so a pathological build can't emit an unbounded
|
|
78
|
+
// line. 20 was too tight — a multi-error build has several "We've found a bug"
|
|
79
|
+
// blocks and the tail (later errors) was silently dropped; keep enough to
|
|
80
|
+
// carry a handful of blocks.
|
|
81
|
+
let maxMessageLines = 200
|
|
82
|
+
|
|
83
|
+
// After output settles, a build that saw an error (and thus no "Finished"
|
|
84
|
+
// terminator) is reported failed, capturing a concise message. The condition
|
|
85
|
+
// is `hasError` alone — not `compiling && hasError` — so an error emitted
|
|
86
|
+
// *before* the first "Parsed …" line (a crashed toolchain, a pnpm/resolution
|
|
87
|
+
// failure) still settles into an `onFail` instead of leaving the consumer
|
|
88
|
+
// pinned at its previous state forever.
|
|
79
89
|
let settle = () =>
|
|
80
|
-
if
|
|
81
|
-
let msg = buffer.contents->Array.slice(~start=0, ~end=
|
|
90
|
+
if hasError.contents {
|
|
91
|
+
let msg = buffer.contents->Array.slice(~start=0, ~end=maxMessageLines)->Array.join("\n")
|
|
82
92
|
clearWatchdog()
|
|
83
93
|
cb.onFail(msg)
|
|
84
94
|
reset()
|
|
@@ -126,7 +136,7 @@ let make = (~watchdogMs: int=120_000, cb: callbacks): (string => unit) => {
|
|
|
126
136
|
}
|
|
127
137
|
} else {
|
|
128
138
|
if String.length(l) > 0 {
|
|
129
|
-
buffer
|
|
139
|
+
buffer.contents->Array.push(l)
|
|
130
140
|
}
|
|
131
141
|
if isErrorMarker(l) {
|
|
132
142
|
hasError := true
|
|
@@ -71,10 +71,10 @@ function make(watchdogMsOpt, cb) {
|
|
|
71
71
|
buffer.contents = [];
|
|
72
72
|
};
|
|
73
73
|
let settle = () => {
|
|
74
|
-
if (!
|
|
74
|
+
if (!hasError.contents) {
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
let msg = buffer.contents.slice(0,
|
|
77
|
+
let msg = buffer.contents.slice(0, 200).join("\n");
|
|
78
78
|
clearWatchdog();
|
|
79
79
|
cb.onFail(msg);
|
|
80
80
|
reset();
|
|
@@ -116,7 +116,7 @@ function make(watchdogMsOpt, cb) {
|
|
|
116
116
|
}
|
|
117
117
|
} else {
|
|
118
118
|
if (l.length > 0) {
|
|
119
|
-
buffer.contents
|
|
119
|
+
buffer.contents.push(l);
|
|
120
120
|
}
|
|
121
121
|
if (isErrorMarker(l)) {
|
|
122
122
|
hasError.contents = true;
|
package/src/Cli.res
CHANGED
|
@@ -31,12 +31,35 @@ type options = {
|
|
|
31
31
|
// the VS Code extension to populate its active-app picker.
|
|
32
32
|
listPlatforms: bool,
|
|
33
33
|
toolVersion: string,
|
|
34
|
+
// When `Some`, `runOnce` runs exactly these compiled test paths instead of
|
|
35
|
+
// walking the tree. Set by the watch loop's worker passes (the parent owns
|
|
36
|
+
// discovery and narrows to the affected package); `None` for the one-shot
|
|
37
|
+
// `run`/`discover` commands, which discover from `roots`.
|
|
38
|
+
paths: option<array<string>>,
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
@val external argv: array<string> = "process.argv"
|
|
37
42
|
|
|
43
|
+
// chokidar reports paths in the same form as its watched roots (relative when
|
|
44
|
+
// roots are `["."]`), while `Discovery` returns absolute paths. Normalize a
|
|
45
|
+
// changed path to absolute before mapping it to an owning package so the
|
|
46
|
+
// prefix match against the discovered set holds.
|
|
47
|
+
@module("node:path") external resolvePath: string => string = "resolve"
|
|
48
|
+
|
|
38
49
|
@val external now: unit => float = "performance.now"
|
|
39
50
|
|
|
51
|
+
type hash
|
|
52
|
+
@module("node:crypto") external createHash: string => hash = "createHash"
|
|
53
|
+
@send external hashUpdate: (hash, string) => hash = "update"
|
|
54
|
+
@send external hashDigest: (hash, string) => string = "digest"
|
|
55
|
+
let sha256 = (s: string): string => createHash("sha256")->hashUpdate(s)->hashDigest("hex")
|
|
56
|
+
|
|
57
|
+
// Last-emitted domain-analysis fingerprint (deadCode + graph + definitions all
|
|
58
|
+
// derive from the loaded plugin structures). Persists across a watch session's
|
|
59
|
+
// repeated discovery emits so an unchanged graph isn't re-sent — a redundant
|
|
60
|
+
// re-emit forces the client to re-render the domain view for nothing.
|
|
61
|
+
let lastDomainHash: ref<option<string>> = ref(None)
|
|
62
|
+
|
|
40
63
|
let dateNowIso: unit => string = %raw(`() => new Date().toISOString()`)
|
|
41
64
|
|
|
42
65
|
let parseFormat = (s: string) =>
|
|
@@ -195,6 +218,7 @@ let parseArgv = (argv: array<string>): result<options, string> => {
|
|
|
195
218
|
uiPorts: uiPorts.contents,
|
|
196
219
|
listPlatforms: listPlatforms.contents,
|
|
197
220
|
toolVersion,
|
|
221
|
+
paths: None,
|
|
198
222
|
})
|
|
199
223
|
}
|
|
200
224
|
}
|
|
@@ -410,17 +434,63 @@ let emitResult = (opts: options, r: RunnerTypes.runResult) =>
|
|
|
410
434
|
if opts.stream {
|
|
411
435
|
FormatterJson.streamRunEnd(r)
|
|
412
436
|
} else {
|
|
413
|
-
FormatterJson.emit(r, ~toolVersion=opts.toolVersion)
|
|
437
|
+
FormatterJson.emit(r, ~toolVersion=opts.toolVersion, ~schemaVersion=?opts.schemaVersion)
|
|
414
438
|
}
|
|
415
439
|
| Tap => FormatterTap.emit(r)
|
|
416
440
|
| Junit => FormatterJunit.emit(r)
|
|
417
441
|
| VsCode => FormatterVsCode.runEnd(r.summary)
|
|
418
442
|
}
|
|
419
443
|
|
|
444
|
+
// Watch re-run scope. A plain edit under a test-owning package narrows the pass
|
|
445
|
+
// to that package's test files (`RunPackages`); structural rebuilds, new-file
|
|
446
|
+
// adds, and any non-VsCode watch use `RunAll`. Scopes accumulate while a pass is
|
|
447
|
+
// in flight — `RunAll` is absorbing (an all-run unioned with anything is an
|
|
448
|
+
// all-run), and `RunPackages` unions its dirs.
|
|
449
|
+
type runScope = RunAll | RunPackages(array<string>)
|
|
450
|
+
|
|
451
|
+
let mergeScope = (a: runScope, b: runScope): runScope =>
|
|
452
|
+
switch (a, b) {
|
|
453
|
+
| (RunAll, _) | (_, RunAll) => RunAll
|
|
454
|
+
| (RunPackages(x), RunPackages(y)) =>
|
|
455
|
+
let seen = Dict.make()
|
|
456
|
+
let union = []
|
|
457
|
+
Array.concat(x, y)->Array.forEach(d =>
|
|
458
|
+
switch seen->Dict.get(d) {
|
|
459
|
+
| Some(_) => ()
|
|
460
|
+
| None => {
|
|
461
|
+
seen->Dict.set(d, true)
|
|
462
|
+
union->Array.push(d)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
RunPackages(union)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// True when `path` sits inside directory `dir` (dir is an ancestor). Lets a
|
|
470
|
+
// package dir be mapped to its test files by prefix, with no per-path fs walk.
|
|
471
|
+
let pathUnderDir = (path: string, dir: string): bool =>
|
|
472
|
+
String.startsWith(path, String.endsWith(dir, "/") ? dir : dir ++ "/")
|
|
473
|
+
|
|
474
|
+
// The subset of `allPaths` a scope re-runs.
|
|
475
|
+
let scopeSubset = (scope: runScope, allPaths: array<string>): array<string> =>
|
|
476
|
+
switch scope {
|
|
477
|
+
| RunAll => allPaths
|
|
478
|
+
| RunPackages(dirs) => allPaths->Array.filter(p => dirs->Array.some(d => pathUnderDir(p, d)))
|
|
479
|
+
}
|
|
480
|
+
|
|
420
481
|
let runOnce = async (opts: options): int => {
|
|
421
|
-
|
|
482
|
+
// A watch worker pass supplies the exact path set (parent-owned discovery,
|
|
483
|
+
// narrowed to the affected package); the one-shot commands discover the tree.
|
|
484
|
+
let paths = switch opts.paths {
|
|
485
|
+
| Some(p) => p
|
|
486
|
+
| None => await Discovery.discover(opts.roots)
|
|
487
|
+
}
|
|
422
488
|
if opts.format == Json && opts.stream {
|
|
423
|
-
FormatterJson.streamRunStart(
|
|
489
|
+
FormatterJson.streamRunStart(
|
|
490
|
+
~toolVersion=opts.toolVersion,
|
|
491
|
+
~startedAt=dateNowIso(),
|
|
492
|
+
~schemaVersion=?opts.schemaVersion,
|
|
493
|
+
)
|
|
424
494
|
}
|
|
425
495
|
let onFileFinished: option<RunnerTypes.fileResult => unit> = switch (
|
|
426
496
|
opts.format,
|
|
@@ -482,15 +552,22 @@ let emitDomainAnalysis = async (pkgs: array<PackageScan.pkg>): unit =>
|
|
|
482
552
|
| Some(platformModulePath) =>
|
|
483
553
|
try {
|
|
484
554
|
let loaded = await LocalHost.loadGraph(~platformModulePath, ~plugins)
|
|
485
|
-
FormatterVsCode.deadCode(DomainDeadCode.analyze(~structures=loaded.structures))
|
|
486
|
-
FormatterVsCode.graph(DomainGraph.build(~structures=loaded.structures))
|
|
487
555
|
// Field schemas for command/event/read-side state (Phase 6.3): the same
|
|
488
556
|
// per-plugin entry the Platform_ComponentDefinitions GraphQL query returns.
|
|
489
|
-
|
|
557
|
+
let definitions =
|
|
490
558
|
loaded.structures->Array.map(((pluginId, s)) =>
|
|
491
559
|
ReventlessCore.Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId, s)
|
|
492
|
-
)
|
|
493
|
-
|
|
560
|
+
)
|
|
561
|
+
// deadCode + graph + definitions are all pure functions of `structures`,
|
|
562
|
+
// so the definitions JSON is a faithful fingerprint of all three. Skip
|
|
563
|
+
// the whole emit when it hasn't moved since the last discovery.
|
|
564
|
+
let fingerprint = sha256(JSON.stringify(JSON.Array(definitions)))
|
|
565
|
+
if lastDomainHash.contents != Some(fingerprint) {
|
|
566
|
+
lastDomainHash := Some(fingerprint)
|
|
567
|
+
FormatterVsCode.deadCode(DomainDeadCode.analyze(~structures=loaded.structures))
|
|
568
|
+
FormatterVsCode.graph(DomainGraph.build(~structures=loaded.structures))
|
|
569
|
+
FormatterVsCode.definitions(definitions)
|
|
570
|
+
}
|
|
494
571
|
} catch {
|
|
495
572
|
| _ => ()
|
|
496
573
|
}
|
|
@@ -561,10 +638,18 @@ let startBuildWatchers = (paths: array<string>): unit => {
|
|
|
561
638
|
|
|
562
639
|
let runWatch = async (opts: options): int => {
|
|
563
640
|
let roots = opts.roots
|
|
641
|
+
// Parent-owned discovery set for the VS Code engine: walked once here and
|
|
642
|
+
// refreshed only on structural rebuilds / new-file adds. A plain edit's re-run
|
|
643
|
+
// pass reuses it (no per-pass tree re-walk) and narrows to the edited file's
|
|
644
|
+
// owning test package (`affectedScope`), so an unrelated plugin's tests aren't
|
|
645
|
+
// re-executed. The per-test NDJSON protocol keeps the client's other results
|
|
646
|
+
// untouched across a narrowed pass.
|
|
647
|
+
let discoveredPaths: ref<array<string>> = ref([])
|
|
564
648
|
// For the VS Code client, watch mode is the whole engine: emit the test tree
|
|
565
649
|
// and package set, take over the ReScript builds, then run + re-run on change.
|
|
566
650
|
if opts.format == VsCode {
|
|
567
651
|
let paths = await Discovery.discover(roots)
|
|
652
|
+
discoveredPaths := paths
|
|
568
653
|
await emitDiscovery(paths)
|
|
569
654
|
startBuildWatchers(paths)
|
|
570
655
|
}
|
|
@@ -582,9 +667,12 @@ let runWatch = async (opts: options): int => {
|
|
|
582
667
|
| None => ()
|
|
583
668
|
}
|
|
584
669
|
)
|
|
585
|
-
|
|
670
|
+
// Run one pass in a fresh worker over `paths`. `None` lets the worker discover
|
|
671
|
+
// the full tree itself — kept for a human terminal `watch`, whose full-summary
|
|
672
|
+
// output must always reflect every file.
|
|
673
|
+
let runInWorker = (paths: option<array<string>>): promise<unit> =>
|
|
586
674
|
Promise.make((resolve, _reject) => {
|
|
587
|
-
let w = Worker.make(Worker.runWorkerUrl, {workerData: opts})
|
|
675
|
+
let w = Worker.make(Worker.runWorkerUrl, {workerData: {...opts, paths}})
|
|
588
676
|
currentWorker := Some(w)
|
|
589
677
|
let settled = ref(false)
|
|
590
678
|
let settle = () =>
|
|
@@ -599,27 +687,50 @@ let runWatch = async (opts: options): int => {
|
|
|
599
687
|
settle()
|
|
600
688
|
})
|
|
601
689
|
})
|
|
690
|
+
// Resolve a scope to the concrete path subset for a worker pass. The VS Code
|
|
691
|
+
// engine passes the parent-owned subset; a human terminal `watch` passes None
|
|
692
|
+
// (worker discovers), preserving its full-summary semantics.
|
|
693
|
+
let pathsForScope = (scope: runScope): option<array<string>> =>
|
|
694
|
+
opts.format == VsCode ? Some(scopeSubset(scope, discoveredPaths.contents)) : None
|
|
695
|
+
// Single-flight run state machine. A re-run requested while a pass is in flight
|
|
696
|
+
// coalesces into `pendingScope` (scopes merge; `RunAll` absorbing) and drains
|
|
697
|
+
// after the current pass. Holding `runInProgress` across the whole drain loop
|
|
698
|
+
// means only one worker ever runs at a time — a third concurrent request can't
|
|
699
|
+
// corrupt the run or interleave NDJSON.
|
|
602
700
|
let runInProgress = ref(false)
|
|
603
|
-
let
|
|
604
|
-
let
|
|
701
|
+
let pendingScope: ref<option<runScope>> = ref(None)
|
|
702
|
+
let requestRun = async (scope: runScope) =>
|
|
605
703
|
if runInProgress.contents {
|
|
606
|
-
|
|
704
|
+
pendingScope :=
|
|
705
|
+
Some(
|
|
706
|
+
switch pendingScope.contents {
|
|
707
|
+
| Some(s) => mergeScope(s, scope)
|
|
708
|
+
| None => scope
|
|
709
|
+
},
|
|
710
|
+
)
|
|
607
711
|
} else {
|
|
608
712
|
runInProgress := true
|
|
609
|
-
let _ = await runInWorker()
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
713
|
+
let _ = await runInWorker(pathsForScope(scope))
|
|
714
|
+
let rec drain = async () =>
|
|
715
|
+
switch pendingScope.contents {
|
|
716
|
+
| Some(next) => {
|
|
717
|
+
pendingScope := None
|
|
718
|
+
let _ = await runInWorker(pathsForScope(next))
|
|
719
|
+
await drain()
|
|
720
|
+
}
|
|
721
|
+
| None => runInProgress := false
|
|
722
|
+
}
|
|
723
|
+
await drain()
|
|
724
|
+
}
|
|
725
|
+
let _ = await requestRun(RunAll)
|
|
726
|
+
// Re-walk discovery (VS Code) then re-run everything. Used after a structural
|
|
727
|
+
// clean-rebuild, whose relocation may have changed the discovered test set.
|
|
728
|
+
let rediscoverAndRun = async () => {
|
|
729
|
+
if opts.format == VsCode {
|
|
730
|
+
discoveredPaths := (await Discovery.discover(roots))
|
|
620
731
|
}
|
|
732
|
+
let _ = await requestRun(RunAll)
|
|
621
733
|
}
|
|
622
|
-
let _ = await run()
|
|
623
734
|
// A *relocation* (a module moved to another directory — a chapter rename /
|
|
624
735
|
// ungroup / move-to-chapter in the extension, or a terminal `mv`) leaves
|
|
625
736
|
// dependents' emitted `.res.mjs` pointing at the old path: incremental
|
|
@@ -640,7 +751,7 @@ let runWatch = async (opts: options): int => {
|
|
|
640
751
|
let rebuilding: Dict.t<bool> = Dict.make()
|
|
641
752
|
let structuralRebuild = (path: string): unit =>
|
|
642
753
|
switch PackageScan.findOwning(path) {
|
|
643
|
-
| None => ignore(
|
|
754
|
+
| None => ignore(requestRun(RunAll))
|
|
644
755
|
| Some(pkg) if rebuilding->Dict.get(pkg.dir)->Option.getOr(false) => ()
|
|
645
756
|
| Some(pkg) =>
|
|
646
757
|
rebuilding->Dict.set(pkg.dir, true)
|
|
@@ -674,7 +785,7 @@ let runWatch = async (opts: options): int => {
|
|
|
674
785
|
} else if !sawError.contents {
|
|
675
786
|
FormatterVsCode.buildFail(pkg.dir, "clean rebuild failed")
|
|
676
787
|
}
|
|
677
|
-
ignore(
|
|
788
|
+
ignore(rediscoverAndRun())
|
|
678
789
|
},
|
|
679
790
|
)
|
|
680
791
|
}
|
|
@@ -684,17 +795,34 @@ let runWatch = async (opts: options): int => {
|
|
|
684
795
|
// (re)claim build watchers for any new package before re-running.
|
|
685
796
|
let refreshDiscovery = async () => {
|
|
686
797
|
let paths = await Discovery.discover(roots)
|
|
798
|
+
discoveredPaths := paths
|
|
687
799
|
await emitDiscovery(paths)
|
|
688
800
|
startBuildWatchers(paths)
|
|
689
|
-
|
|
801
|
+
let _ = await requestRun(RunAll)
|
|
690
802
|
}
|
|
803
|
+
// Narrow a plain edit's re-run to the edited file's owning test package (VS
|
|
804
|
+
// Code only). `RunAll` when the file has no owning package, or the owner isn't
|
|
805
|
+
// one of the discovered test packages (e.g. a framework edit that ripples into
|
|
806
|
+
// several plugins — full re-run is the safe choice there). A human terminal
|
|
807
|
+
// `watch` always re-runs everything (full-summary output).
|
|
808
|
+
let affectedScope = (path: string): runScope =>
|
|
809
|
+
if opts.format == VsCode {
|
|
810
|
+
let abs = resolvePath(path)
|
|
811
|
+
switch PackageScan.findOwning(PackageScan.dirname(abs)) {
|
|
812
|
+
| Some(pkg) if discoveredPaths.contents->Array.some(p => pathUnderDir(p, pkg.dir)) =>
|
|
813
|
+
RunPackages([pkg.dir])
|
|
814
|
+
| _ => RunAll
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
RunAll
|
|
818
|
+
}
|
|
691
819
|
let _watcher = Watch.start(roots, (event, path) =>
|
|
692
820
|
if opts.format == VsCode && Watch.isStructuralSource(event, path) {
|
|
693
821
|
structuralRebuild(path)
|
|
694
822
|
} else if opts.format == VsCode && event == Watch.Add && path->String.endsWith(".res") {
|
|
695
823
|
ignore(refreshDiscovery())
|
|
696
824
|
} else {
|
|
697
|
-
ignore(
|
|
825
|
+
ignore(requestRun(affectedScope(path)))
|
|
698
826
|
}
|
|
699
827
|
)
|
|
700
828
|
// Keep the process alive until cancelled. Awaiting here (rather than letting
|
package/src/Cli.res.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Nodepath from "node:path";
|
|
4
|
+
import * as Nodecrypto from "node:crypto";
|
|
3
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
6
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
9
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
10
|
import * as Watch$ReventlessGwt from "./Watch.res.mjs";
|
|
8
11
|
import * as Nodeworker_threads from "node:worker_threads";
|
|
@@ -33,6 +36,14 @@ import * as Platform_ComponentDefinitionsApi$ReventlessCore from "@reventlessdev
|
|
|
33
36
|
|
|
34
37
|
let toolVersion = "0.1.0";
|
|
35
38
|
|
|
39
|
+
function sha256(s) {
|
|
40
|
+
return Nodecrypto.createHash("sha256").update(s).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let lastDomainHash = {
|
|
44
|
+
contents: undefined
|
|
45
|
+
};
|
|
46
|
+
|
|
36
47
|
let dateNowIso = (() => new Date().toISOString());
|
|
37
48
|
|
|
38
49
|
function parseFormat(s) {
|
|
@@ -294,7 +305,8 @@ Exit code is 1 if any test failed, 0 otherwise.
|
|
|
294
305
|
backend: backend,
|
|
295
306
|
uiPorts: uiPorts,
|
|
296
307
|
listPlatforms: listPlatforms,
|
|
297
|
-
toolVersion: toolVersion
|
|
308
|
+
toolVersion: toolVersion,
|
|
309
|
+
paths: undefined
|
|
298
310
|
}
|
|
299
311
|
};
|
|
300
312
|
}
|
|
@@ -496,7 +508,7 @@ function emitResult(opts, r) {
|
|
|
496
508
|
if (opts.stream) {
|
|
497
509
|
return FormatterJson$ReventlessGwt.streamRunEnd(r);
|
|
498
510
|
} else {
|
|
499
|
-
return FormatterJson$ReventlessGwt.emit(r, opts.toolVersion);
|
|
511
|
+
return FormatterJson$ReventlessGwt.emit(r, opts.toolVersion, opts.schemaVersion);
|
|
500
512
|
}
|
|
501
513
|
case "Tap" :
|
|
502
514
|
return FormatterTap$ReventlessGwt.emit(r);
|
|
@@ -507,10 +519,48 @@ function emitResult(opts, r) {
|
|
|
507
519
|
}
|
|
508
520
|
}
|
|
509
521
|
|
|
522
|
+
function mergeScope(a, b) {
|
|
523
|
+
if (typeof a !== "object") {
|
|
524
|
+
return "RunAll";
|
|
525
|
+
}
|
|
526
|
+
if (typeof b !== "object") {
|
|
527
|
+
return "RunAll";
|
|
528
|
+
}
|
|
529
|
+
let seen = {};
|
|
530
|
+
let union = [];
|
|
531
|
+
a._0.concat(b._0).forEach(d => {
|
|
532
|
+
let match = seen[d];
|
|
533
|
+
if (match !== undefined) {
|
|
534
|
+
return;
|
|
535
|
+
} else {
|
|
536
|
+
seen[d] = true;
|
|
537
|
+
union.push(d);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
TAG: "RunPackages",
|
|
543
|
+
_0: union
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function pathUnderDir(path, dir) {
|
|
548
|
+
return path.startsWith(dir.endsWith("/") ? dir : dir + "/");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function scopeSubset(scope, allPaths) {
|
|
552
|
+
if (typeof scope !== "object") {
|
|
553
|
+
return allPaths;
|
|
554
|
+
}
|
|
555
|
+
let dirs = scope._0;
|
|
556
|
+
return allPaths.filter(p => dirs.some(d => pathUnderDir(p, d)));
|
|
557
|
+
}
|
|
558
|
+
|
|
510
559
|
async function runOnce(opts) {
|
|
511
|
-
let
|
|
560
|
+
let p = opts.paths;
|
|
561
|
+
let paths = p !== undefined ? p : await Discovery$ReventlessGwt.discover(opts.roots);
|
|
512
562
|
if (opts.format === "Json" && opts.stream) {
|
|
513
|
-
FormatterJson$ReventlessGwt.streamRunStart(opts.toolVersion, dateNowIso());
|
|
563
|
+
FormatterJson$ReventlessGwt.streamRunStart(opts.toolVersion, dateNowIso(), opts.schemaVersion);
|
|
514
564
|
}
|
|
515
565
|
let match = opts.format;
|
|
516
566
|
let match$1 = opts.stream;
|
|
@@ -557,9 +607,16 @@ async function emitDomainAnalysis(pkgs) {
|
|
|
557
607
|
}
|
|
558
608
|
try {
|
|
559
609
|
let loaded = await LocalHost$ReventlessGwt.loadGraph(platformModulePath, plugins);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
610
|
+
let definitions = loaded.structures.map(param => Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(param[0], param[1]));
|
|
611
|
+
let fingerprint = sha256(JSON.stringify(definitions));
|
|
612
|
+
if (Primitive_object.notequal(lastDomainHash.contents, fingerprint)) {
|
|
613
|
+
lastDomainHash.contents = fingerprint;
|
|
614
|
+
FormatterVsCode$ReventlessGwt.deadCode(DomainDeadCode$ReventlessGwt.analyze(loaded.structures));
|
|
615
|
+
FormatterVsCode$ReventlessGwt.graph(DomainGraph$ReventlessGwt.build(loaded.structures));
|
|
616
|
+
return FormatterVsCode$ReventlessGwt.definitions(definitions);
|
|
617
|
+
} else {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
563
620
|
} catch (exn) {
|
|
564
621
|
return;
|
|
565
622
|
}
|
|
@@ -620,8 +677,12 @@ function startBuildWatchers(paths) {
|
|
|
620
677
|
|
|
621
678
|
async function runWatch(opts) {
|
|
622
679
|
let roots = opts.roots;
|
|
680
|
+
let discoveredPaths = {
|
|
681
|
+
contents: []
|
|
682
|
+
};
|
|
623
683
|
if (opts.format === "VsCode") {
|
|
624
684
|
let paths = await Discovery$ReventlessGwt.discover(roots);
|
|
685
|
+
discoveredPaths.contents = paths;
|
|
625
686
|
await emitDiscovery(paths);
|
|
626
687
|
startBuildWatchers(paths);
|
|
627
688
|
}
|
|
@@ -635,9 +696,22 @@ async function runWatch(opts) {
|
|
|
635
696
|
return;
|
|
636
697
|
}
|
|
637
698
|
});
|
|
638
|
-
let runInWorker =
|
|
699
|
+
let runInWorker = paths => new Promise((resolve, _reject) => {
|
|
639
700
|
let w = new Nodeworker_threads.Worker(Worker$ReventlessGwt.runWorkerUrl, {
|
|
640
|
-
workerData:
|
|
701
|
+
workerData: {
|
|
702
|
+
subcommand: opts.subcommand,
|
|
703
|
+
format: opts.format,
|
|
704
|
+
stream: opts.stream,
|
|
705
|
+
watch: opts.watch,
|
|
706
|
+
filters: opts.filters,
|
|
707
|
+
schemaVersion: opts.schemaVersion,
|
|
708
|
+
roots: opts.roots,
|
|
709
|
+
backend: opts.backend,
|
|
710
|
+
uiPorts: opts.uiPorts,
|
|
711
|
+
listPlatforms: opts.listPlatforms,
|
|
712
|
+
toolVersion: opts.toolVersion,
|
|
713
|
+
paths: paths
|
|
714
|
+
}
|
|
641
715
|
});
|
|
642
716
|
currentWorker.contents = Primitive_option.some(w);
|
|
643
717
|
let settled = {
|
|
@@ -656,32 +730,67 @@ async function runWatch(opts) {
|
|
|
656
730
|
settle();
|
|
657
731
|
});
|
|
658
732
|
});
|
|
733
|
+
let pathsForScope = scope => {
|
|
734
|
+
if (opts.format === "VsCode") {
|
|
735
|
+
return scopeSubset(scope, discoveredPaths.contents);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
659
738
|
let runInProgress = {
|
|
660
739
|
contents: false
|
|
661
740
|
};
|
|
662
|
-
let
|
|
663
|
-
contents:
|
|
741
|
+
let pendingScope = {
|
|
742
|
+
contents: undefined
|
|
664
743
|
};
|
|
665
|
-
let
|
|
744
|
+
let requestRun = async scope => {
|
|
666
745
|
if (runInProgress.contents) {
|
|
667
|
-
|
|
746
|
+
let s = pendingScope.contents;
|
|
747
|
+
pendingScope.contents = s !== undefined ? mergeScope(s, scope) : scope;
|
|
668
748
|
return;
|
|
669
749
|
}
|
|
670
750
|
runInProgress.contents = true;
|
|
671
|
-
await runInWorker();
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
751
|
+
await runInWorker(pathsForScope(scope));
|
|
752
|
+
let drain = async () => {
|
|
753
|
+
let next = pendingScope.contents;
|
|
754
|
+
if (next !== undefined) {
|
|
755
|
+
pendingScope.contents = undefined;
|
|
756
|
+
await runInWorker(pathsForScope(next));
|
|
757
|
+
return await drain();
|
|
758
|
+
} else {
|
|
759
|
+
runInProgress.contents = false;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
675
762
|
};
|
|
676
|
-
|
|
763
|
+
return await drain();
|
|
764
|
+
};
|
|
765
|
+
await requestRun("RunAll");
|
|
766
|
+
let rediscoverAndRun = async () => {
|
|
767
|
+
if (opts.format === "VsCode") {
|
|
768
|
+
discoveredPaths.contents = await Discovery$ReventlessGwt.discover(roots);
|
|
769
|
+
}
|
|
770
|
+
await requestRun("RunAll");
|
|
677
771
|
};
|
|
678
|
-
await run();
|
|
679
772
|
let rebuilding = {};
|
|
680
773
|
let refreshDiscovery = async () => {
|
|
681
774
|
let paths = await Discovery$ReventlessGwt.discover(roots);
|
|
775
|
+
discoveredPaths.contents = paths;
|
|
682
776
|
await emitDiscovery(paths);
|
|
683
777
|
startBuildWatchers(paths);
|
|
684
|
-
|
|
778
|
+
await requestRun("RunAll");
|
|
779
|
+
};
|
|
780
|
+
let affectedScope = path => {
|
|
781
|
+
if (opts.format !== "VsCode") {
|
|
782
|
+
return "RunAll";
|
|
783
|
+
}
|
|
784
|
+
let abs = Nodepath.resolve(path);
|
|
785
|
+
let pkg = PackageScan$ReventlessGwt.findOwning(Nodepath.dirname(abs));
|
|
786
|
+
if (pkg !== undefined && discoveredPaths.contents.some(p => pathUnderDir(p, pkg.dir))) {
|
|
787
|
+
return {
|
|
788
|
+
TAG: "RunPackages",
|
|
789
|
+
_0: [pkg.dir]
|
|
790
|
+
};
|
|
791
|
+
} else {
|
|
792
|
+
return "RunAll";
|
|
793
|
+
}
|
|
685
794
|
};
|
|
686
795
|
Watch$ReventlessGwt.start(roots, (event, path) => {
|
|
687
796
|
if (opts.format === "VsCode" && Watch$ReventlessGwt.isStructuralSource(event, path)) {
|
|
@@ -711,16 +820,16 @@ async function runWatch(opts) {
|
|
|
711
820
|
} else if (!sawError.contents) {
|
|
712
821
|
FormatterVsCode$ReventlessGwt.buildFail(pkg.dir, "clean rebuild failed");
|
|
713
822
|
}
|
|
714
|
-
|
|
823
|
+
rediscoverAndRun();
|
|
715
824
|
});
|
|
716
825
|
}
|
|
717
|
-
|
|
826
|
+
requestRun("RunAll");
|
|
718
827
|
return;
|
|
719
828
|
} else {
|
|
720
829
|
if (opts.format === "VsCode" && event === "Add" && path.endsWith(".res")) {
|
|
721
830
|
refreshDiscovery();
|
|
722
831
|
} else {
|
|
723
|
-
|
|
832
|
+
requestRun(affectedScope(path));
|
|
724
833
|
}
|
|
725
834
|
return;
|
|
726
835
|
}
|
|
@@ -812,6 +921,8 @@ let defaultTimeoutMs = 5000;
|
|
|
812
921
|
|
|
813
922
|
export {
|
|
814
923
|
toolVersion,
|
|
924
|
+
sha256,
|
|
925
|
+
lastDomainHash,
|
|
815
926
|
dateNowIso,
|
|
816
927
|
parseFormat,
|
|
817
928
|
defaultRoots,
|
|
@@ -825,6 +936,9 @@ export {
|
|
|
825
936
|
loadAndCollect,
|
|
826
937
|
runFiles,
|
|
827
938
|
emitResult,
|
|
939
|
+
mergeScope,
|
|
940
|
+
pathUnderDir,
|
|
941
|
+
scopeSubset,
|
|
828
942
|
runOnce,
|
|
829
943
|
emitDomainAnalysis,
|
|
830
944
|
emitDiscovery,
|
|
@@ -835,4 +949,4 @@ export {
|
|
|
835
949
|
listPlatforms,
|
|
836
950
|
main,
|
|
837
951
|
}
|
|
838
|
-
/*
|
|
952
|
+
/* node:path Not a pure module */
|
package/src/Collector.res
CHANGED
|
@@ -108,7 +108,7 @@ let push = (
|
|
|
108
108
|
location,
|
|
109
109
|
timeout,
|
|
110
110
|
}
|
|
111
|
-
entries
|
|
111
|
+
entries.contents->Array.push(entry)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// A `todo` placeholder: a spec-less slice still ships a discoverable, compiling
|
|
@@ -127,7 +127,7 @@ let pushTodo = (name: string) => {
|
|
|
127
127
|
location: None,
|
|
128
128
|
timeout: None,
|
|
129
129
|
}
|
|
130
|
-
entries
|
|
130
|
+
entries.contents->Array.push(entry)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
let drain = () => {
|
package/src/Collector.res.mjs
CHANGED
|
@@ -105,7 +105,7 @@ function push(slice, location, timeout, name, body) {
|
|
|
105
105
|
location: location,
|
|
106
106
|
timeout: timeout
|
|
107
107
|
};
|
|
108
|
-
entries.contents
|
|
108
|
+
entries.contents.push(entry);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function pushTodo(name) {
|
|
@@ -122,7 +122,7 @@ function pushTodo(name) {
|
|
|
122
122
|
location: undefined,
|
|
123
123
|
timeout: undefined
|
|
124
124
|
};
|
|
125
|
-
entries.contents
|
|
125
|
+
entries.contents.push(entry);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
function drain() {
|