@uniflowed/router 0.0.0-alpha.18 → 0.0.0-alpha.21

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.
@@ -0,0 +1,557 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/router`: which DOM subtree each boundary owns.
4
+ //
5
+ // An application has three boundaries a reader cannot see — Suspense, error,
6
+ // and client/server — and all three are decisions the build already made.
7
+ // ubugeeei-prod/uf#636 answered the third: `uf dev` says why a module is in the
8
+ // client bundle, out loud, at the moment the answer changes. This is the other
9
+ // two, and the gap it fills was named in that issue's triage: *the route table
10
+ // already has the data*. `$loading.js` nests and carries the number of
11
+ // layouts outside it, `$error.js` binds to the nearest ancestor, and
12
+ // `RouteView` threads both into one stack. What did not exist is a way to point
13
+ // at the **DOM subtree** each of them owns.
14
+ //
15
+ // That cannot be read off the table, and it cannot be read off the page either.
16
+ // A `<Suspense>` renders no element of its own; nor does a class boundary. What
17
+ // is on the page is a run of nodes, in a parent that also holds whatever the
18
+ // layout above put beside them — a `<nav>` before, a `<footer>` after — and
19
+ // nothing distinguishes the run from its neighbours. So the render has to say
20
+ // so, which is what this module is.
21
+ //
22
+ // # The mechanism: a pair of inert marks, and why a pair
23
+ //
24
+ // Each boundary `RouteView` renders wraps its children between two
25
+ // `<span hidden data-uf-boundary>` elements. The `hidden` attribute keeps the
26
+ // marks out of layout and accessibility trees; the pair are siblings of the nodes between them,
27
+ // because React fragments create no element, so "what this boundary owns" is
28
+ // `open.nextElementSibling` up to `close`.
29
+ //
30
+ // One mark would have been cheaper and would have been wrong. A boundary's run
31
+ // ends where the enclosing layout's own trailing nodes begin, and from the
32
+ // opening mark alone those are indistinguishable — the walk would hand a
33
+ // boundary the footer underneath it. Two marks are the smallest thing that
34
+ // closes.
35
+ //
36
+ // A wrapper element was the other candidate: one node instead of two, and
37
+ // `wrapper.children` with no walk at all. It loses on the thing that matters
38
+ // here — a wrapper has to exist from the first render, because introducing one
39
+ // later moves the subtree into a new parent and React answers that by
40
+ // unmounting and rebuilding everything under it. Marks are siblings, so they
41
+ // can arrive after the page has settled, which is what the section below is
42
+ // about.
43
+ //
44
+ // # They arrive after hydration, which is what makes them free of it
45
+ //
46
+ // Every edge renders `null` until it has mounted. So the tree React hydrates
47
+ // against the server's markup contains no mark, the server's markup contains no
48
+ // mark, and the two agree whatever either side believed about being in
49
+ // development — the gate is allowed to answer differently in the two processes
50
+ // because by the time it has any effect, hydration is over. `reportDevtools`
51
+ // asks its question on the line after hydration for the same reason: a
52
+ // development affordance that can turn a working page into a mismatch is worse
53
+ // than no affordance.
54
+ //
55
+ // `marksAreLive` is what keeps that from costing a second commit forever. It is
56
+ // latched by the first edge to mount, and every edge mounted afterwards — a
57
+ // navigation, or a `$loading.js` that HMR has just added — starts live and
58
+ // is in the DOM in the same commit that created it. That matters for the report
59
+ // below, which reads the DOM in the commit where the boundaries changed.
60
+ //
61
+ // # Where the report goes
62
+ //
63
+ // The terminal, on `./diagnostics.js`, which is the channel #583 established
64
+ // and #636 argued for again: a diagnostic that exists only in a browser window
65
+ // has to be noticed by somebody who does not know to look. And it is quiet
66
+ // unless the answer *changed* — the first sighting of a route says nothing, the
67
+ // same boundaries on the same route say nothing, and adding an `$error.js`
68
+ // says where it landed and what it took over. A boundary map printed on every
69
+ // reload is the banner nobody reads.
70
+ //
71
+ // The marks themselves are the other half of "see it", and the cheaper half:
72
+ // they are in the document, so the element inspector already shows where each
73
+ // boundary opens and closes with no panel to open, and
74
+ // `document.querySelectorAll("[data-uf-boundary]")` is the whole API. For the
75
+ // page you are looking at right now there is `__ufBoundaries()`, which prints
76
+ // the same report on demand.
77
+ //
78
+ // # None of it is in a build
79
+ //
80
+ // Nothing here is reachable from a production bundle: `runtime.js` guards every
81
+ // reference with `BOUNDARY_MARKS`, which is `import.meta.hot != null` — the
82
+ // gate `client.js` already uses, replaced by `undefined` in a build — and this
83
+ // package is `sideEffects: false`, so with the references folded away the
84
+ // module is dropped rather than merely unused.
85
+
86
+ import * as React from "react";
87
+ import { useEffect, useState } from "react";
88
+
89
+ import { reportDiagnostic } from "./diagnostics.js";
90
+
91
+ /** The attribute a mark carries its boundary's id in. */
92
+ export const BOUNDARY_ATTRIBUTE: string = "data-uf-boundary";
93
+
94
+ /** The attribute telling the two marks of one boundary apart. */
95
+ export const EDGE_ATTRIBUTE: string = "data-uf-boundary-edge";
96
+
97
+ /** The attribute naming the file a boundary was declared in, when one is known. */
98
+ export const SOURCE_ATTRIBUTE: string = "data-uf-boundary-source";
99
+
100
+ /** The name `uf dev` installs the on-demand report under. */
101
+ export const BOUNDARY_GLOBAL: string = "__ufBoundaries";
102
+
103
+ /**
104
+ * What `file` says for the error boundary the build synthesises.
105
+ *
106
+ * `routesModuleSource` writes this string where a declared boundary has a path,
107
+ * because the record has no module to name — the framework's own error page
108
+ * renders in its place. Written out again rather than imported for the reason
109
+ * `./devtools.js` gives about `DEVTOOLS_HOOK`: `@uniflowed/vite` is plain
110
+ * JavaScript loaded by Vite before any Flow transform exists, so the import
111
+ * cannot go either way. `boundaries.test.js` holds the two spellings together.
112
+ */
113
+ export const SYNTHESISED_SOURCE: string = "@uniflowed/router";
114
+
115
+ /** Which kind of boundary a mark belongs to. */
116
+ export type BoundaryKind = "suspense" | "error";
117
+
118
+ /**
119
+ * One boundary of a resolved route, as the marks and the report name it.
120
+ *
121
+ * `id` pairs the two marks and is stable for a boundary across renders, so a
122
+ * navigation that keeps a boundary keeps its marks mounted. `above` is how many
123
+ * of the route's layouts are outside it — the same number, spelled the same
124
+ * way, that `ResolvedRoute["errorBoundary"].above` and every `loading` entry
125
+ * carry, because there is no second vocabulary for where a thing sits in the
126
+ * stack.
127
+ *
128
+ * `source` is the file it was declared in, and is `null` for every `<Suspense>`
129
+ * boundary. That asymmetry is the route table's rather than this module's: an
130
+ * error boundary is matched by path, so the table carries its `file`, while a
131
+ * `$loading.js` is carried by depth alone. Adding a path to the loading
132
+ * records would put one in every visitor's bundle to serve a report only
133
+ * `uf dev` reads.
134
+ */
135
+ export type RouteBoundary = {|
136
+ readonly id: string,
137
+ readonly kind: BoundaryKind,
138
+ readonly above: number,
139
+ readonly source: ?string,
140
+ |};
141
+
142
+ /** The id of the `<Suspense>` boundary at `index` of a route's `loading`. */
143
+ export function suspenseId(index: number): string {
144
+ return `suspense:${index}`;
145
+ }
146
+
147
+ /** The id of the boundary a route's own `$error.js` renders. */
148
+ export const ROUTE_ERROR_ID: string = "error:route";
149
+
150
+ /**
151
+ * The id of the boundary that stands outside every layout.
152
+ *
153
+ * It has no module and renders the framework's page; it is what is between a
154
+ * throw in a root layout, or in the error component itself, and an unmounted
155
+ * document. Marked like any other, because "which subtree does the last resort
156
+ * own" is exactly as unanswerable from the page as the rest.
157
+ */
158
+ export const ROOT_ERROR_ID: string = "error:root";
159
+
160
+ /** The part of a resolved route this module reads. */
161
+ type BoundedRoute = {
162
+ readonly errorBoundary: { readonly above: number, ... },
163
+ readonly loading: $ReadOnlyArray<{ readonly above: number, ... }>,
164
+ readonly error: mixed,
165
+ ...
166
+ };
167
+
168
+ /**
169
+ * Every boundary a resolved route renders, in the order they nest.
170
+ *
171
+ * A `Map` rather than a list because it is read both ways: `RouteView` asks for
172
+ * one by id as it builds the stack, and the report walks the values. One
173
+ * function answering both is the point — an id `RouteView` marks and the report
174
+ * cannot find is a boundary that silently disappears from the map, and
175
+ * ubugeeei-prod/uf#636 made the same argument about an explanation that can
176
+ * disagree with the thing it explains.
177
+ *
178
+ * The route's own error boundary is absent when the route *is* its error page,
179
+ * which is exactly when `RouteView` does not render one: wrapping that page in
180
+ * the boundary whose component it is would answer a throw inside it with
181
+ * itself.
182
+ *
183
+ * @param resolved the route being rendered
184
+ * @param errorSource the `file` of the nearest `$error.js`, when the table
185
+ * has one; `null` leaves the boundary named by its depth alone
186
+ */
187
+ export function routeBoundaries(
188
+ resolved: BoundedRoute,
189
+ errorSource: ?string,
190
+ ): Map<string, RouteBoundary> {
191
+ const found: Map<string, RouteBoundary> = new Map();
192
+ found.set(ROOT_ERROR_ID, {
193
+ id: ROOT_ERROR_ID,
194
+ kind: "error",
195
+ above: 0,
196
+ source: SYNTHESISED_SOURCE,
197
+ });
198
+ if (resolved.error == null) {
199
+ found.set(ROUTE_ERROR_ID, {
200
+ id: ROUTE_ERROR_ID,
201
+ kind: "error",
202
+ above: resolved.errorBoundary.above,
203
+ source: errorSource,
204
+ });
205
+ }
206
+ resolved.loading.forEach((boundary, index) => {
207
+ const id = suspenseId(index);
208
+ found.set(id, { id, kind: "suspense", above: boundary.above, source: null });
209
+ });
210
+ return found;
211
+ }
212
+
213
+ /**
214
+ * Whether an edge that mounts now should be in the DOM immediately.
215
+ *
216
+ * Latched by the first edge to mount and never cleared. Read through
217
+ * `useState`'s initialiser rather than during the render body, which is the
218
+ * difference between "this component's first state" and "a module variable a
219
+ * memoising compiler is entitled to hold on to".
220
+ */
221
+ let marksAreLive = false;
222
+
223
+ /**
224
+ * One end of one boundary.
225
+ *
226
+ * A hidden element and not a comment node, because React renders elements.
227
+ * This used to be a `<template>`, but React 19.3 reports template insertion
228
+ * during document-root hydration as a browser error. A `span hidden` carries
229
+ * the same marker data without entering layout or the accessibility tree.
230
+ */
231
+ component BoundaryEdge(boundary: RouteBoundary, edge: "open" | "close") {
232
+ const [live, setLive] = useState<boolean>(() => marksAreLive);
233
+ useEffect(() => {
234
+ marksAreLive = true;
235
+ setLive(true);
236
+ }, []);
237
+ if (!live) {
238
+ return null;
239
+ }
240
+ if (edge === "close") {
241
+ return <span hidden data-uf-boundary={boundary.id} data-uf-boundary-edge="close" />;
242
+ }
243
+ return (
244
+ <span
245
+ hidden
246
+ data-uf-boundary={boundary.id}
247
+ data-uf-boundary-edge="open"
248
+ data-uf-boundary-source={boundary.source ?? undefined}
249
+ />
250
+ );
251
+ }
252
+
253
+ /**
254
+ * `children`, between the two marks of `boundary`.
255
+ *
256
+ * `children` unchanged when there is no boundary to mark, so a caller never has
257
+ * to ask twice. The marks are the first and last children of a fragment rather
258
+ * than a wrapper's, so the nodes between them are siblings of them, and every
259
+ * position in the fragment is fixed — an edge going from `null` to a hidden
260
+ * mark after mount is an insertion beside `children` and not around it,
261
+ * which is why it costs no remount.
262
+ */
263
+ export function insideBoundary(boundary: ?RouteBoundary, children: React.Node): React.Node {
264
+ if (boundary == null) {
265
+ return children;
266
+ }
267
+ return (
268
+ <>
269
+ <BoundaryEdge boundary={boundary} edge="open" />
270
+ {children}
271
+ <BoundaryEdge boundary={boundary} edge="close" />
272
+ </>
273
+ );
274
+ }
275
+
276
+ /**
277
+ * How far a walk between two marks will go before giving up.
278
+ *
279
+ * A closing mark is a sibling of its opening one and the run between them is a
280
+ * route's rendered output, so this is never reached by a page that is behaving.
281
+ * It is here because `docs/security.md` asks that a report have no unbounded
282
+ * anything in it, and because a DOM somebody else's script has been editing is
283
+ * exactly where an unbounded walk would be found.
284
+ */
285
+ const WALK_LIMIT = 512;
286
+
287
+ /** How many owned elements one line of the report names before it counts them. */
288
+ const NAMED_LIMIT = 3;
289
+
290
+ /** One boundary, as the page has it. */
291
+ export type BoundaryFinding = {|
292
+ readonly boundary: RouteBoundary,
293
+ /** The top-level elements between its marks, as short selectors. */
294
+ readonly owns: $ReadOnlyArray<string>,
295
+ /** How many more there were than [`NAMED_LIMIT`]. */
296
+ readonly more: number,
297
+ /** False when the boundary rendered no marks — it is showing its fallback. */
298
+ readonly rendered: boolean,
299
+ |};
300
+
301
+ /**
302
+ * What each boundary owns on the page right now.
303
+ *
304
+ * Takes the document rather than reaching for a global, so a test can build one
305
+ * and ask — the same shape `hydrationReport` has, and for the same reason: the
306
+ * analysis worth checking is the one that runs in a browser, so the test has to
307
+ * be able to call exactly it.
308
+ *
309
+ * A boundary with no marks in the document is not missing, it is *suspended*:
310
+ * React removes a boundary's content while its fallback is up, and its marks
311
+ * are part of that content. Saying so is more useful than leaving it out.
312
+ */
313
+ export function boundaryFindings(
314
+ boundaries: Map<string, RouteBoundary>,
315
+ document: Document,
316
+ ): $ReadOnlyArray<BoundaryFinding> {
317
+ const findings: Array<BoundaryFinding> = [];
318
+ for (const boundary of boundaries.values()) {
319
+ const open = document.querySelector(
320
+ `[${BOUNDARY_ATTRIBUTE}="${boundary.id}"][${EDGE_ATTRIBUTE}="open"]`,
321
+ );
322
+ if (open == null) {
323
+ findings.push({ boundary, owns: [], more: 0, rendered: false });
324
+ continue;
325
+ }
326
+ const owned: Array<string> = [];
327
+ let steps = 0;
328
+ let node = open.nextElementSibling;
329
+ while (node != null && steps < WALK_LIMIT && !closes(node, boundary.id)) {
330
+ if (node.getAttribute(BOUNDARY_ATTRIBUTE) == null) {
331
+ owned.push(describeElement(node));
332
+ }
333
+ node = node.nextElementSibling;
334
+ steps += 1;
335
+ }
336
+ findings.push({
337
+ boundary,
338
+ owns: owned.slice(0, NAMED_LIMIT),
339
+ more: Math.max(0, owned.length - NAMED_LIMIT),
340
+ rendered: true,
341
+ });
342
+ }
343
+ return findings;
344
+ }
345
+
346
+ /** Whether `element` is the closing mark of `id`. */
347
+ function closes(element: Element, id: string): boolean {
348
+ return (
349
+ element.getAttribute(BOUNDARY_ATTRIBUTE) === id &&
350
+ element.getAttribute(EDGE_ATTRIBUTE) === "close"
351
+ );
352
+ }
353
+
354
+ /**
355
+ * One element, short enough to sit in a line of a report.
356
+ *
357
+ * A CSS selector rather than a tag name, because a page has eleven `<div>`s and
358
+ * the one being named has to be findable: the id if it has one, and otherwise
359
+ * the first class, which is what a person would type into the inspector's
360
+ * search box. Nothing more — the report says which subtree, and the page says
361
+ * what is in it.
362
+ */
363
+ export function describeElement(element: Element): string {
364
+ const tag = element.tagName.toLowerCase();
365
+ const id = element.getAttribute("id");
366
+ if (id != null && id !== "") {
367
+ return `${tag}#${id}`;
368
+ }
369
+ const className = element.getAttribute("class");
370
+ const first = className == null ? "" : className.trim().split(/\s+/)[0];
371
+ return first === "" ? tag : `${tag}.${first}`;
372
+ }
373
+
374
+ /**
375
+ * The report, as the terminal will print it.
376
+ *
377
+ * Separated from the sending so that a test can pin the wording, which is the
378
+ * part worth pinning: somebody reading this in a terminal has to be able to act
379
+ * on it without opening this file.
380
+ */
381
+ export function formatBoundaries(
382
+ path: string,
383
+ findings: $ReadOnlyArray<BoundaryFinding>,
384
+ ): {| readonly message: string, readonly detail: $ReadOnlyArray<string> |} {
385
+ const count = findings.length;
386
+ return {
387
+ message: `${count} ${count === 1 ? "boundary renders" : "boundaries render"} ${path}`,
388
+ detail: findings.map(describeFinding),
389
+ };
390
+ }
391
+
392
+ /** One boundary as one line: what it is, where it sits, and what it owns. */
393
+ function describeFinding(finding: BoundaryFinding): string {
394
+ const { boundary } = finding;
395
+ const kind = boundary.kind === "error" ? "error" : "suspense";
396
+ const source =
397
+ boundary.source == null
398
+ ? ""
399
+ : boundary.source === SYNTHESISED_SOURCE
400
+ ? " (uf's own error page)"
401
+ : ` (${boundary.source})`;
402
+ const where =
403
+ boundary.above === 0
404
+ ? "outside every layout"
405
+ : `inside ${boundary.above} ${boundary.above === 1 ? "layout" : "layouts"}`;
406
+ if (!finding.rendered) {
407
+ return `${kind}${source}, ${where} — showing its fallback`;
408
+ }
409
+ if (finding.owns.length === 0) {
410
+ return `${kind}${source}, ${where} — owns no element of its own`;
411
+ }
412
+ const named = finding.owns.join(", ");
413
+ const more = finding.more === 0 ? "" : ` and ${finding.more} more`;
414
+ return `${kind}${source}, ${where} — owns ${named}${more}`;
415
+ }
416
+
417
+ /**
418
+ * How many routes the "has this changed" memory keeps.
419
+ *
420
+ * A route table is finite and this is larger than any project's hot set, so the
421
+ * ceiling is `docs/security.md`'s rule rather than a policy about routes: a map
422
+ * a page can grow by navigating is a map with a bound.
423
+ */
424
+ const MEMORY_LIMIT = 64;
425
+
426
+ /** The last boundary set seen for each route path. */
427
+ const seen: Map<string, string> = new Map();
428
+
429
+ /** What the on-demand report reads; the reporter keeps it current. */
430
+ let current: {| readonly path: string, readonly boundaries: Map<string, RouteBoundary> |} | null =
431
+ null;
432
+
433
+ /**
434
+ * The boundary set as one comparable string.
435
+ *
436
+ * Kind, depth and source — everything a reader would notice — and not what the
437
+ * page currently owns: a boundary whose subtree changed because the route's
438
+ * data changed has not changed, and reporting it would make this fire on every
439
+ * keystroke behind a search box.
440
+ */
441
+ function signature(boundaries: Map<string, RouteBoundary>): string {
442
+ return [...boundaries.values()].map((it) => `${it.id}@${it.above}:${it.source ?? ""}`).join("|");
443
+ }
444
+
445
+ /**
446
+ * Send the report for whatever is on the page now.
447
+ *
448
+ * Exported for [`BOUNDARY_GLOBAL`] and used by the reporter, so the on-demand
449
+ * answer and the automatic one are the same sentence about the same page.
450
+ */
451
+ export function reportBoundaries(
452
+ path: string,
453
+ boundaries: Map<string, RouteBoundary>,
454
+ document: Document,
455
+ ): $ReadOnlyArray<BoundaryFinding> {
456
+ const findings = boundaryFindings(boundaries, document);
457
+ const { message, detail } = formatBoundaries(path, findings);
458
+ reportDiagnostic({ severity: "info", message, detail });
459
+ return findings;
460
+ }
461
+
462
+ /**
463
+ * Watches the boundary set and says when it changed.
464
+ *
465
+ * Renders nothing, and its effect has no dependency list on purpose: the
466
+ * question is asked after every commit, and the answer is a string comparison
467
+ * over a handful of entries before anything touches the DOM. The document is
468
+ * read only in the commit that is about to be reported, which is also the
469
+ * commit the marks are in — every edge mounted after the first one starts live,
470
+ * so a boundary that has just appeared is in the page by the time this runs.
471
+ *
472
+ * Quiet on the first sighting of a route, for the reason ubugeeei-prod/uf#636
473
+ * is quiet on the first scan: a listing of everything, at the moment somebody
474
+ * loaded a page, is not a thing anybody asked.
475
+ */
476
+ export component BoundaryReporter(path: string, boundaries: Map<string, RouteBoundary>) {
477
+ useEffect(() => {
478
+ current = { path, boundaries };
479
+ installOnDemand();
480
+ const next = signature(boundaries);
481
+ const previous = seen.get(path);
482
+ if (seen.size >= MEMORY_LIMIT && previous === undefined) {
483
+ const oldest = seen.keys().next();
484
+ if (!oldest.done) {
485
+ seen.delete(oldest.value);
486
+ }
487
+ }
488
+ seen.set(path, next);
489
+ if (previous === undefined || previous === next) {
490
+ return;
491
+ }
492
+ const document = globalThis.document;
493
+ if (document == null) {
494
+ return;
495
+ }
496
+ reportBoundaries(path, boundaries, document);
497
+ });
498
+ return null;
499
+ }
500
+
501
+ /**
502
+ * The global object, under the one description this module has of it.
503
+ *
504
+ * A read-only indexer, which is what makes the annotation assignable at all:
505
+ * `globalThis` is a namespace to the checker, and every one of its members is
506
+ * read-only, so a writable indexer disagrees with all of them at once. The same
507
+ * shape `@uniflowed/react-testing`'s `internal/dom.js` reads globals through,
508
+ * and for the same reason — a name in, and no claim about what comes out.
509
+ */
510
+ type Globals = { readonly [string]: mixed };
511
+
512
+ /** The global object, for reading. */
513
+ const globals: Globals = globalThis;
514
+
515
+ /**
516
+ * Install `__ufBoundaries()`, once.
517
+ *
518
+ * The answer to "and how do I see the page I am looking at *now*", which the
519
+ * change-driven report deliberately does not give. A function on the global
520
+ * rather than a key binding or a panel: there is nothing to discover by
521
+ * accident, nothing to intercept a page's own keystrokes, and the console is
522
+ * already open in the window this is about. It returns the findings as well as
523
+ * printing them, so the browser shows the tree and the terminal keeps the line.
524
+ *
525
+ * Defined rather than assigned, for the reason `internal/dom.js` gives about
526
+ * `navigator`: a name the host declared as an accessor cannot be assigned to,
527
+ * and a development affordance must not be able to throw on a page.
528
+ */
529
+ function installOnDemand(): void {
530
+ if (globals[BOUNDARY_GLOBAL] != null) {
531
+ return;
532
+ }
533
+ Object.defineProperty(globalThis, BOUNDARY_GLOBAL, {
534
+ value: () => {
535
+ const live = current;
536
+ const document = globalThis.document;
537
+ if (live == null || document == null) {
538
+ return [];
539
+ }
540
+ return reportBoundaries(live.path, live.boundaries, document);
541
+ },
542
+ writable: true,
543
+ configurable: true,
544
+ });
545
+ }
546
+
547
+ /**
548
+ * Forget every route this module has seen.
549
+ *
550
+ * For tests, which share one module registry across files and would otherwise
551
+ * inherit a route's history from whichever file rendered it first.
552
+ */
553
+ export function forgetBoundaries(): void {
554
+ seen.clear();
555
+ current = null;
556
+ marksAreLive = false;
557
+ }
@@ -29,7 +29,7 @@
29
29
  // panel props, hooks and source positions — is not checked here because a
30
30
  // browser cannot tell the difference from the outside, and because uf owns it
31
31
  // end to end: `mode` is `development` and `uf transform` is called with
32
- // `development: true`, both asserted in `tests/library/devtools.test.js`
32
+ // `development: true`, both asserted in `packages/vite/devtools.test.js`
33
33
  // against the plugin rather than against a page. What is left is what only a
34
34
  // running page knows.
35
35
  //
@@ -53,7 +53,7 @@ import { reportDiagnostic } from "./diagnostics.js";
53
53
  * that file's neighbour `internal/diagnostics.js` gives about the endpoint
54
54
  * paths: `@uniflowed/vite` is loaded by Vite before any Flow transform exists
55
55
  * and this module is Flow, so the import cannot go either way.
56
- * `tests/library/devtools.test.js` asserts the two spellings agree, which is
56
+ * `packages/vite/devtools.test.js` asserts the two spellings agree, which is
57
57
  * what makes a duplicated constant honest.
58
58
  */
59
59
  export const DEVTOOLS_HOOK: string = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
@@ -29,7 +29,7 @@
29
29
  // published package that depends on an unpublished one, because the tarball
30
30
  // would name a version the registry does not have; `@uniflowed/router` is on
31
31
  // npm and `@uniflowed/hmr` is a declaration package that is not. What the two
32
- // posters do share is the contract, and `tests/library/dev-channel.test.js`
32
+ // posters do share is the contract, and `packages/vite/dev-channel.test.js`
33
33
  // asserts that every spelling of these paths agrees — a duplicated constant
34
34
  // with a test on it is honest, and one without is how a browser ends up
35
35
  // posting to a path nothing serves.