@uniflowed/vite 0.0.0-alpha.12 → 0.0.0-alpha.14
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/driver.js +329 -145
- package/index.js +266 -51
- package/internal/diagnostics.js +366 -0
- package/internal/events.js +5 -5
- package/internal/routes.js +295 -27
- package/internal/rsc.js +256 -1
- package/internal/serve.js +46 -3
- package/package.json +11 -3
package/index.js
CHANGED
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
// development and the virtual modules that make a directory
|
|
15
15
|
// of pages an application: the route table, the client entry
|
|
16
16
|
// that hydrates it, and the server entry that renders it. In
|
|
17
|
-
// development it also
|
|
18
|
-
//
|
|
17
|
+
// development it also answers every request the way a
|
|
18
|
+
// deployment does — the guard, a server action, a route
|
|
19
|
+
// handler, then the renderer — so `uf dev` serves what
|
|
20
|
+
// `uf start` serves rather than an approximation of it, and it
|
|
21
|
+
// serves the two `/__uf/` paths a browser reports to (see
|
|
22
|
+
// `internal/diagnostics.js`).
|
|
19
23
|
// The client's copy of the route table is not the server's:
|
|
20
24
|
// `internal/rsc.js` reads the RSC analysis and leaves out the
|
|
21
25
|
// page of every route no client boundary reaches, so that
|
|
@@ -55,7 +59,16 @@ import {
|
|
|
55
59
|
preambleCode,
|
|
56
60
|
refreshRuntimeSource,
|
|
57
61
|
} from "./internal/refresh.js";
|
|
58
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
RSC_MANIFEST_ENV,
|
|
64
|
+
actionReferenceSource,
|
|
65
|
+
actionsModuleSource,
|
|
66
|
+
clientRouteFilter,
|
|
67
|
+
readRscManifest,
|
|
68
|
+
rscManifestKey,
|
|
69
|
+
serverActionModules,
|
|
70
|
+
serverActionTable,
|
|
71
|
+
} from "./internal/rsc.js";
|
|
59
72
|
import {
|
|
60
73
|
RESERVED,
|
|
61
74
|
VIRTUAL,
|
|
@@ -65,8 +78,9 @@ import {
|
|
|
65
78
|
serverModuleSource,
|
|
66
79
|
} from "./internal/routes.js";
|
|
67
80
|
import { TransformService, isFlowModule } from "@uniflowed/host/transform";
|
|
81
|
+
import { createChannelMiddleware } from "./internal/diagnostics.js";
|
|
68
82
|
import { send, toRequest } from "./internal/http.js";
|
|
69
|
-
import {
|
|
83
|
+
import { beginRequest } from "./internal/serve.js";
|
|
70
84
|
|
|
71
85
|
/** A resolved virtual id: Vite's convention is a leading NUL byte. */
|
|
72
86
|
const resolved = (id) => `\0${id}`;
|
|
@@ -185,7 +199,44 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
185
199
|
if (server == null) {
|
|
186
200
|
emit("rsc-split", { pages: kept.size, routes: table.routes.length });
|
|
187
201
|
}
|
|
188
|
-
|
|
202
|
+
// `relativeTo` only here, and never for the server's copy below. Every
|
|
203
|
+
// `import()` in this table becomes a chunk URL, but `file` is a string and
|
|
204
|
+
// survives the build — so the browser's table was shipping the absolute
|
|
205
|
+
// path of every page on the machine that built the site, to every visitor.
|
|
206
|
+
// The server's table is read where those files are and keeps them.
|
|
207
|
+
return routesModuleSource(table, {
|
|
208
|
+
shipsPage: (route) => kept.has(route),
|
|
209
|
+
relativeTo: root,
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The manifest's two action tables, re-read only when the file changes.
|
|
215
|
+
*
|
|
216
|
+
* `load` runs for every module in the graph and has to ask "is this a
|
|
217
|
+
* `"use server"` module?" about each one, so parsing the manifest per call
|
|
218
|
+
* would put a JSON parse of the whole graph between Vite and every file it
|
|
219
|
+
* opens. Size and modification time are the identity — the same pair
|
|
220
|
+
* `.uf/cache/transform` keys on — and `uf dev` drops the memo outright when
|
|
221
|
+
* its watcher sees the file change, so a rewrite inside one millisecond is
|
|
222
|
+
* still seen.
|
|
223
|
+
*/
|
|
224
|
+
let actionMemo = null;
|
|
225
|
+
const forgetActions = () => {
|
|
226
|
+
actionMemo = null;
|
|
227
|
+
};
|
|
228
|
+
const actionTables = () => {
|
|
229
|
+
const file = process.env[RSC_MANIFEST_ENV];
|
|
230
|
+
const key = rscManifestKey(file);
|
|
231
|
+
if (actionMemo == null || actionMemo.key !== key) {
|
|
232
|
+
const manifest = readRscManifest(file);
|
|
233
|
+
actionMemo = {
|
|
234
|
+
key,
|
|
235
|
+
modules: serverActionModules(manifest, root),
|
|
236
|
+
table: serverActionTable(manifest, root),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return actionMemo;
|
|
189
240
|
};
|
|
190
241
|
|
|
191
242
|
return {
|
|
@@ -256,7 +307,36 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
256
307
|
}
|
|
257
308
|
if (id === resolved(VIRTUAL.client)) return clientModuleSource(entryPath);
|
|
258
309
|
if (id === resolved(VIRTUAL.server)) return serverModuleSource(entryPath);
|
|
310
|
+
// Only `virtual:uf/server` imports this, so it is only ever asked for in
|
|
311
|
+
// the server environment — but the table it carries is every callable
|
|
312
|
+
// endpoint of the build, so it is worth saying that a browser asking for
|
|
313
|
+
// it gets nothing rather than getting the list.
|
|
314
|
+
if (id === resolved(VIRTUAL.actions)) {
|
|
315
|
+
if (!isSsr(this, loadOptions))
|
|
316
|
+
return "export const actions = [];\nexport default actions;\n";
|
|
317
|
+
return actionsModuleSource(actionTables().table);
|
|
318
|
+
}
|
|
259
319
|
if (id.startsWith(STYLE_PREFIX)) return styles.get(id) ?? "";
|
|
320
|
+
|
|
321
|
+
// A `"use server"` module, in the browser's graph only: what the client
|
|
322
|
+
// gets is one `createServerReference` per callable export, and never the
|
|
323
|
+
// file. This is where the second half of the RSC split actually happens
|
|
324
|
+
// — the route filter above decides which *pages* the browser is given,
|
|
325
|
+
// and this decides that an action module's body, its imports and
|
|
326
|
+
// everything only they reached are not the browser's business at all.
|
|
327
|
+
//
|
|
328
|
+
// Substituting the source rather than rewriting it: a transform that
|
|
329
|
+
// stripped the body would have to be right about every way a module can
|
|
330
|
+
// name something, and being wrong once means shipping a database handle.
|
|
331
|
+
// The exports the reference module declares come from the manifest, so
|
|
332
|
+
// they are exactly the exports `uf_rsc` decided are callable endpoints
|
|
333
|
+
// and an import of anything else is a build error rather than a silent
|
|
334
|
+
// `undefined`. `crates/uf_rsc/src/graph/build.rs` colours these modules
|
|
335
|
+
// server for the same reason, so the analysis and the bundle agree.
|
|
336
|
+
if (!isSsr(this, loadOptions)) {
|
|
337
|
+
const references = actionTables().modules.get(cleanId(id));
|
|
338
|
+
if (references != null) return actionReferenceSource(references);
|
|
339
|
+
}
|
|
260
340
|
return null;
|
|
261
341
|
},
|
|
262
342
|
|
|
@@ -378,8 +458,15 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
378
458
|
devServer.watcher.add(manifestPath);
|
|
379
459
|
const onManifest = (file) => {
|
|
380
460
|
if (path.resolve(file) !== manifestPath) return;
|
|
461
|
+
// The action tables are read from the same file and are memoised on
|
|
462
|
+
// its size and modification time, which is a pair two writes inside
|
|
463
|
+
// one millisecond can share. This is the answer that does not
|
|
464
|
+
// depend on a clock.
|
|
465
|
+
forgetActions();
|
|
381
466
|
const routes = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.routes));
|
|
382
467
|
if (routes) devServer.moduleGraph.invalidateModule(routes);
|
|
468
|
+
const actions = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.actions));
|
|
469
|
+
if (actions) devServer.moduleGraph.invalidateModule(actions);
|
|
383
470
|
devServer.ws.send({ type: "full-reload", path: "*" });
|
|
384
471
|
};
|
|
385
472
|
devServer.watcher.on("add", onManifest);
|
|
@@ -387,75 +474,181 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
387
474
|
}
|
|
388
475
|
|
|
389
476
|
// After Vite's own middlewares, so `/@vite/client`, `/@id/...` and
|
|
390
|
-
// static files are served first and only
|
|
391
|
-
//
|
|
477
|
+
// static files are served first and only what Vite declined reaches uf.
|
|
478
|
+
//
|
|
479
|
+
// # One renderer
|
|
480
|
+
//
|
|
481
|
+
// This is the only middleware that renders a document under `uf dev`,
|
|
482
|
+
// and that is worth stating because there were two. `driver.js` added a
|
|
483
|
+
// second one after `createServer` had returned, which put it *later* in
|
|
484
|
+
// the connect stack than this one — so for every request this one
|
|
485
|
+
// claimed, this one decided, and the other was reached only for what
|
|
486
|
+
// this one declined. Route-handler dispatch was in the other. A
|
|
487
|
+
// `GET /feed` from a browser is `Accept: text/html` with no extension,
|
|
488
|
+
// so it looked like a document, so `app/feed/_uf.route.js` was never
|
|
489
|
+
// asked and the reader got the route table's page — or the not-found
|
|
490
|
+
// page — for a path that had a handler. See ubugeeei-prod/uf#349.
|
|
491
|
+
//
|
|
492
|
+
// Two renderers is also how the two came to disagree about the render
|
|
493
|
+
// result's `headers`: this one wrote them and the driver's did not, so a
|
|
494
|
+
// loader calling `redirect()` answered a browser with a `307` carrying
|
|
495
|
+
// no `Location` and a meta-refresh body — a redirect that works when you
|
|
496
|
+
// deploy it and not while you are writing it. See
|
|
497
|
+
// ubugeeei-prod/uf#338.
|
|
498
|
+
//
|
|
499
|
+
// So the driver's middleware is gone and everything it did is here: it
|
|
500
|
+
// claims every request, runs the guard, the action endpoint and the
|
|
501
|
+
// dispatcher for every method, and hands back to Vite's chain what none
|
|
502
|
+
// of them answered.
|
|
503
|
+
//
|
|
504
|
+
// # What is still not `createFetchHandler`
|
|
505
|
+
//
|
|
506
|
+
// One middleware rather than two, and still not the function every
|
|
507
|
+
// deployment runs. It cannot be: `transformIndexHtml` takes a whole
|
|
508
|
+
// document, so the render has to be collected here rather than streamed
|
|
509
|
+
// (ubugeeei-prod/uf#374), and a request nothing claimed has to go back
|
|
510
|
+
// to Vite's chain rather than become a 404 — neither of which a handler
|
|
511
|
+
// that always answers with a `Response` can do.
|
|
512
|
+
//
|
|
513
|
+
// What that still costs, written down so the next reader does not have
|
|
514
|
+
// to find it: `createFetchHandler` renders inside a cache scope even
|
|
515
|
+
// with no cache configured, so a component calling `cacheLife` states a
|
|
516
|
+
// lifetime nobody honours; here there is no scope, so the same component
|
|
517
|
+
// throws under `uf dev` and renders under `uf start`. Closing that means
|
|
518
|
+
// the generated server entry handing the host a scope the way it already
|
|
519
|
+
// hands it `beginRequest` — see `serverModuleSource` in
|
|
520
|
+
// `./internal/routes.js` — and it is the next thing to remove from this
|
|
521
|
+
// list rather than something this middleware can decide on its own.
|
|
392
522
|
return () => {
|
|
523
|
+
// The browser's own reporting channel, mounted above the application
|
|
524
|
+
// so that a report never reaches a project's `_uf.middleware.js` or
|
|
525
|
+
// its route table. See `internal/diagnostics.js`.
|
|
526
|
+
devServer.middlewares.use(
|
|
527
|
+
createChannelMiddleware((diagnostic) => emit("diagnostic", diagnostic)),
|
|
528
|
+
);
|
|
529
|
+
|
|
393
530
|
devServer.middlewares.use(async (request, response, next) => {
|
|
394
|
-
|
|
531
|
+
// `request.url` and not `originalUrl`, which is the URL Vite's base
|
|
532
|
+
// middleware has already stripped the base from — and the route
|
|
533
|
+
// table's paths have no base in them either.
|
|
534
|
+
const url = request.url ?? "/";
|
|
535
|
+
// Declared out here so the catch below can still settle: a request
|
|
536
|
+
// that failed is a request that happened, and a middleware that
|
|
537
|
+
// logged its arrival is owed its callback either way.
|
|
538
|
+
let lifecycle = null;
|
|
395
539
|
try {
|
|
396
|
-
const url = request.url ?? "/";
|
|
397
540
|
const entry = await importServerEntry(devServer);
|
|
398
541
|
const asRequest = await toRequest(request, devServer.config);
|
|
399
542
|
|
|
400
|
-
//
|
|
401
|
-
// written
|
|
402
|
-
// `
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// ubugeeei-prod/uf#260.
|
|
414
|
-
// every method.
|
|
543
|
+
// The request begins here and ends when the response has been
|
|
544
|
+
// written, which is what `after()` promises and what `uf preview`,
|
|
545
|
+
// `uf start` and a compiled binary all do too — a middleware that
|
|
546
|
+
// logs a response's status has to mean the same thing in
|
|
547
|
+
// development as in production. See `internal/serve.js` and
|
|
548
|
+
// ubugeeei-prod/uf#389.
|
|
549
|
+
lifecycle = await beginRequest(entry, asRequest);
|
|
550
|
+
const answered = await lifecycle.run(async () => {
|
|
551
|
+
// Before anything answers: a middleware guards a subtree, so it
|
|
552
|
+
// has to run for a page, for a route handler, and for a path
|
|
553
|
+
// under it that matches neither. Running it inside the
|
|
554
|
+
// dispatcher and again inside the renderer would have left
|
|
555
|
+
// `/dashboard/typo` unguarded and run it twice for a path that
|
|
556
|
+
// is both. See ubugeeei-prod/uf#260.
|
|
415
557
|
const guarded = await entry.runMiddleware(asRequest);
|
|
416
558
|
if (guarded != null) {
|
|
417
559
|
await send(response, guarded);
|
|
418
|
-
return;
|
|
560
|
+
return true;
|
|
419
561
|
}
|
|
420
562
|
|
|
421
|
-
// Then
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
563
|
+
// Then a server action, below the guard and above the handlers.
|
|
564
|
+
// It declines every request that carries no action id, so this
|
|
565
|
+
// costs an ordinary request one `headers.get`; and it answers
|
|
566
|
+
// every request that carries one, refusals included, so an
|
|
567
|
+
// action can never fall through to a route handler that happens
|
|
568
|
+
// to sit at the URL it was posted to. The same two lines are in
|
|
569
|
+
// `@uniflowed/server`'s `fetch.js`, which is what every
|
|
570
|
+
// deployment runs.
|
|
571
|
+
const acted = await entry.callAction(asRequest);
|
|
572
|
+
if (acted != null) {
|
|
573
|
+
await send(response, acted);
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Then the route handlers, above the renderer and for every
|
|
578
|
+
// method: a path that answers a request is not a document,
|
|
579
|
+
// whatever the client said it would accept. `curl /api/thing`
|
|
580
|
+
// and a `<form action>` navigation both send `Accept:
|
|
581
|
+
// text/html`, and both want the handler's answer — which is the
|
|
582
|
+
// whole of ubugeeei-prod/uf#349.
|
|
435
583
|
const handled = await entry.dispatch(asRequest);
|
|
436
584
|
if (handled != null) {
|
|
437
585
|
await send(response, handled);
|
|
438
|
-
return;
|
|
586
|
+
return true;
|
|
439
587
|
}
|
|
440
588
|
|
|
589
|
+
// Only a navigation reaches the renderer. A page cannot answer a
|
|
590
|
+
// `POST`, and letting one try would turn a missing handler into
|
|
591
|
+
// a rendered page with a 200 rather than a 404.
|
|
592
|
+
//
|
|
593
|
+
// `wantsDocument` is stricter than the production handler, which
|
|
594
|
+
// renders anything a static file did not answer, and the
|
|
595
|
+
// difference is Vite's chain: `/@id/…`, `/node_modules/…` and
|
|
596
|
+
// any path with an extension belong to the module server, and a
|
|
597
|
+
// request one of those declined has to go back to it rather than
|
|
598
|
+
// become a rendered 404 page. What it costs is that
|
|
599
|
+
// `/favicon.svg` on a project that has none is a bare 404 here
|
|
600
|
+
// and the project's own not-found *page* under `uf preview` and
|
|
601
|
+
// `uf start` — a difference in the body of a 404 for a path that
|
|
602
|
+
// is an asset request in the first place.
|
|
603
|
+
if (!wantsDocument(request)) return false;
|
|
604
|
+
|
|
441
605
|
const result = await entry.render(
|
|
442
606
|
url,
|
|
443
607
|
{ scripts: [devUrlFor(VIRTUAL.client)], styles: [], preloads: [] },
|
|
444
|
-
{
|
|
608
|
+
{
|
|
609
|
+
// A boundary that threw after the shell went out.
|
|
610
|
+
// `result.error` cannot carry it — the caller already has the
|
|
611
|
+
// result by then — so the terminal hears about it here or not
|
|
612
|
+
// at all.
|
|
613
|
+
onError: (error) => reportRenderError(devServer, url, error),
|
|
614
|
+
},
|
|
445
615
|
);
|
|
446
616
|
if (result.error != null) reportRenderError(devServer, url, result.error);
|
|
447
|
-
// Collected rather than piped
|
|
448
|
-
//
|
|
617
|
+
// Collected rather than piped: `transformIndexHtml` is a
|
|
618
|
+
// whole-document hook, so there is no first byte to send until it
|
|
619
|
+
// has run. `uf start` and `uf preview` stream — see
|
|
620
|
+
// `internal/serve.js` — and that is a property of the development
|
|
621
|
+
// server rather than of the renderer. ubugeeei-prod/uf#374.
|
|
449
622
|
const html = await devServer.transformIndexHtml(url, await result.text());
|
|
450
|
-
response.statusCode = result.status;
|
|
451
|
-
|
|
623
|
+
response.statusCode = result.status ?? 200;
|
|
624
|
+
// The render's own headers, then the content type over the top:
|
|
625
|
+
// exactly the order `@uniflowed/server`'s `fetch.js` writes them
|
|
626
|
+
// in, so a `Location` from `redirect()` survives here and a
|
|
627
|
+
// render cannot claim to be something other than a document.
|
|
452
628
|
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
453
629
|
response.setHeader(name, value);
|
|
454
630
|
}
|
|
631
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
455
632
|
response.end(html);
|
|
633
|
+
return true;
|
|
456
634
|
});
|
|
635
|
+
|
|
636
|
+
if (!answered) {
|
|
637
|
+
// The one path where uf is not the one writing the response: a
|
|
638
|
+
// request nothing claimed goes back to Vite's chain. The guard
|
|
639
|
+
// has still run and may have deferred work, so `close` — the
|
|
640
|
+
// socket saying the response is over, however it ended — is the
|
|
641
|
+
// only honest signal left that the bytes are out.
|
|
642
|
+
response.once("close", lifecycle.settle);
|
|
643
|
+
next();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
await lifecycle.settle();
|
|
457
647
|
} catch (error) {
|
|
458
|
-
|
|
648
|
+
if (lifecycle != null) await lifecycle.settle();
|
|
649
|
+
// Map the stack back onto the Flow source before it reaches the
|
|
650
|
+
// overlay.
|
|
651
|
+
if (error instanceof Error) devServer.ssrFixStacktrace(error);
|
|
459
652
|
next(error);
|
|
460
653
|
}
|
|
461
654
|
});
|
|
@@ -526,14 +719,25 @@ const ALL_DIAGNOSTICS = "UF_REACT_COMPILER_DIAGNOSTICS";
|
|
|
526
719
|
* Report what the React Compiler said about one module.
|
|
527
720
|
*
|
|
528
721
|
* Every finding used to be printed as `a function: <message>` — no file, no
|
|
529
|
-
* line, no column, and the fallback string doing all the work because
|
|
530
|
-
*
|
|
722
|
+
* line, no column, and the fallback string doing all the work, because
|
|
723
|
+
* `diagnostic.function` was read out of a field only a *success* event carries
|
|
724
|
+
* and so was null for every finding there has ever been (#371), not because
|
|
725
|
+
* the compiler was reporting on anonymous inner functions. The transform hook
|
|
531
726
|
* knows the module and the compiler gives a position for most findings, so
|
|
532
727
|
* both go into the message: Vite prints a plugin warning's `message` and
|
|
533
728
|
* nothing else, so a location that is not in the string is a location the
|
|
534
729
|
* reader never sees. `id` and `loc` go along for anything reading the log
|
|
535
730
|
* object rather than the line. See ubugeeei-prod/uf#307.
|
|
536
731
|
*
|
|
732
|
+
* `(in Form)` is real now: `uf transform` recovers the name from the tree it
|
|
733
|
+
* compiled, and leaves it off the diagnostic when the function genuinely has
|
|
734
|
+
* no name to give.
|
|
735
|
+
*
|
|
736
|
+
* The position is real too, and is the expression the compiler objected to
|
|
737
|
+
* rather than the function containing it, so the two halves of the line say
|
|
738
|
+
* different things — `packages/hooks/dom.js:195:7: This value cannot be
|
|
739
|
+
* modified (in useLongPress)` locates the write and names the hook to look in.
|
|
740
|
+
*
|
|
537
741
|
* A dependency's findings are held back. A React Compiler bailout inside
|
|
538
742
|
* `@uniflowed/form` is not something the person running the build can fix, and
|
|
539
743
|
* a channel carrying forty of them on every build is a channel people stop
|
|
@@ -556,9 +760,20 @@ function reportDiagnostics(context, { id, root, diagnostics, environment, report
|
|
|
556
760
|
const mine = isProjectModule(root, id) || process.env[ALL_DIAGNOSTICS] === "all";
|
|
557
761
|
for (const diagnostic of diagnostics) {
|
|
558
762
|
// Everything a reader would be shown, so two findings that would print as
|
|
559
|
-
// the same line collapse into one.
|
|
560
|
-
//
|
|
561
|
-
//
|
|
763
|
+
// the same line collapse into one.
|
|
764
|
+
//
|
|
765
|
+
// This used to collapse far more than that, and the note here used to say
|
|
766
|
+
// the compiler reports "Cannot access refs during render" once per pass
|
|
767
|
+
// that noticed it — three times for one `ref`. That was the wrong reading
|
|
768
|
+
// of the evidence. The position in a finding was the position of the
|
|
769
|
+
// *function* it was found in, so every finding in one function shared a
|
|
770
|
+
// line and a column and any two with the same message were, to this
|
|
771
|
+
// signature, the same finding. They were not: over this repository's own
|
|
772
|
+
// packages it collapsed 173 findings into 119 lines, and the five that
|
|
773
|
+
// became one line in `DatePickerInput` are five different reads of a ref
|
|
774
|
+
// on five different lines. `uf transform` now reports where the compiler
|
|
775
|
+
// actually objected, so the signature separates them, and what it still
|
|
776
|
+
// collapses is a genuine repeat of one site.
|
|
562
777
|
const signature = `${diagnostic.kind}\0${diagnostic.line}\0${diagnostic.column}\0${diagnostic.message}`;
|
|
563
778
|
if (ledger.signatures.has(signature)) continue;
|
|
564
779
|
ledger.signatures.add(signature);
|