@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +7 -7
  3. package/src/Behavior_GWT.res +125 -159
  4. package/src/Behavior_GWT.res.mjs +156 -59
  5. package/src/BuildClassifier.res +15 -5
  6. package/src/BuildClassifier.res.mjs +3 -3
  7. package/src/Cli.res +157 -29
  8. package/src/Cli.res.mjs +138 -24
  9. package/src/Collector.res +2 -2
  10. package/src/Collector.res.mjs +2 -2
  11. package/src/ComponentMeta.res +16 -43
  12. package/src/ComponentMeta.res.mjs +12 -41
  13. package/src/Discovery.res +19 -19
  14. package/src/Discovery.res.mjs +5 -9
  15. package/src/DomainGraph.res +0 -0
  16. package/src/DomainGraph.res.mjs +0 -0
  17. package/src/FormatterHuman.res +14 -43
  18. package/src/FormatterHuman.res.mjs +21 -28
  19. package/src/FormatterJson.res +12 -5
  20. package/src/FormatterJson.res.mjs +8 -6
  21. package/src/FormatterTap.res +13 -52
  22. package/src/FormatterTap.res.mjs +25 -43
  23. package/src/FormatterVsCode.res +4 -19
  24. package/src/FormatterVsCode.res.mjs +4 -23
  25. package/src/LocalHost.res +11 -27
  26. package/src/LocalHost.res.mjs +6 -34
  27. package/src/MismatchRender.res +72 -0
  28. package/src/MismatchRender.res.mjs +127 -0
  29. package/src/Outcome.res +0 -64
  30. package/src/Outcome.res.mjs +0 -47
  31. package/src/PlatformScan.res +14 -10
  32. package/src/PlatformScan.res.mjs +14 -13
  33. package/tests/BuildClassifierTest.res +39 -0
  34. package/tests/BuildClassifierTest.res.mjs +44 -0
  35. package/tests/CliWatchScopeTest.res +62 -0
  36. package/tests/CliWatchScopeTest.res.mjs +95 -0
  37. package/tests/ComponentMetaTest.res +15 -0
  38. package/tests/ComponentMetaTest.res.mjs +14 -0
  39. package/tests/DomainGraphTest.res +2 -2
  40. package/tests/FlowAggregateGwtTest.res +2 -0
  41. package/tests/FlowAggregateGwtTest.res.mjs +4 -0
  42. package/tests/FormatterGoldenTest.res +125 -0
  43. package/tests/FormatterGoldenTest.res.mjs +162 -0
  44. package/tests/MappingGwtTest.res +2 -0
  45. package/tests/MappingGwtTest.res.mjs +4 -0
@@ -74,11 +74,21 @@ let make = (~watchdogMs: int=120_000, cb: callbacks): (string => unit) => {
74
74
  buffer := []
75
75
  }
76
76
 
77
- // After output settles, a compile that saw an error (and thus no "Finished"
78
- // terminator) is reported failed, capturing a concise message.
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 compiling.contents && hasError.contents {
81
- let msg = buffer.contents->Array.slice(~start=0, ~end=20)->Array.join("\n")
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 := Array.concat(buffer.contents, [l])
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 (!(compiling.contents && hasError.contents)) {
74
+ if (!hasError.contents) {
75
75
  return;
76
76
  }
77
- let msg = buffer.contents.slice(0, 20).join("\n");
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 = buffer.contents.concat([l]);
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
- let paths = await Discovery.discover(opts.roots)
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(~toolVersion=opts.toolVersion, ~startedAt=dateNowIso())
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
- FormatterVsCode.definitions(
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
- let runInWorker = (): promise<unit> =>
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 rerunPending = ref(false)
604
- let run = async () => {
701
+ let pendingScope: ref<option<runScope>> = ref(None)
702
+ let requestRun = async (scope: runScope) =>
605
703
  if runInProgress.contents {
606
- rerunPending := true
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
- // Drain every re-run request that arrived while a pass was in flight.
611
- // Clearing `runInProgress` before awaiting the pending pass (the previous
612
- // shape) let a third concurrent `run` start mid-drain, which corrupts the
613
- // module-global `Collector` and interleaves NDJSON events. Hold the flag
614
- // across the whole drain loop so only one pass ever runs at a time.
615
- while rerunPending.contents {
616
- rerunPending := false
617
- let _ = await runInWorker()
618
- }
619
- runInProgress := false
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(run())
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(run())
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
- ignore(run())
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(run())
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 paths = await Discovery$ReventlessGwt.discover(opts.roots);
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
- FormatterVsCode$ReventlessGwt.deadCode(DomainDeadCode$ReventlessGwt.analyze(loaded.structures));
561
- FormatterVsCode$ReventlessGwt.graph(DomainGraph$ReventlessGwt.build(loaded.structures));
562
- return FormatterVsCode$ReventlessGwt.definitions(loaded.structures.map(param => Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(param[0], param[1])));
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 = () => new Promise((resolve, _reject) => {
699
+ let runInWorker = paths => new Promise((resolve, _reject) => {
639
700
  let w = new Nodeworker_threads.Worker(Worker$ReventlessGwt.runWorkerUrl, {
640
- workerData: opts
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 rerunPending = {
663
- contents: false
741
+ let pendingScope = {
742
+ contents: undefined
664
743
  };
665
- let run = async () => {
744
+ let requestRun = async scope => {
666
745
  if (runInProgress.contents) {
667
- rerunPending.contents = true;
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
- while (rerunPending.contents) {
673
- rerunPending.contents = false;
674
- await runInWorker();
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
- runInProgress.contents = false;
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
- run();
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
- run();
823
+ rediscoverAndRun();
715
824
  });
716
825
  }
717
- run();
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
- run();
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
- /* Stdlib_JsExn Not a pure module */
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 := Array.concat(entries.contents, [entry])
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 := Array.concat(entries.contents, [entry])
130
+ entries.contents->Array.push(entry)
131
131
  }
132
132
 
133
133
  let drain = () => {
@@ -105,7 +105,7 @@ function push(slice, location, timeout, name, body) {
105
105
  location: location,
106
106
  timeout: timeout
107
107
  };
108
- entries.contents = entries.contents.concat([entry]);
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 = entries.contents.concat([entry]);
125
+ entries.contents.push(entry);
126
126
  }
127
127
 
128
128
  function drain() {