@uniflowed/vite 0.0.0-alpha.13 → 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 +281 -147
- package/index.js +184 -90
- package/internal/diagnostics.js +366 -0
- package/internal/events.js +5 -5
- package/internal/routes.js +44 -6
- package/internal/serve.js +39 -15
- 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
|
|
@@ -56,7 +60,6 @@ import {
|
|
|
56
60
|
refreshRuntimeSource,
|
|
57
61
|
} from "./internal/refresh.js";
|
|
58
62
|
import {
|
|
59
|
-
ACTION_HEADER,
|
|
60
63
|
RSC_MANIFEST_ENV,
|
|
61
64
|
actionReferenceSource,
|
|
62
65
|
actionsModuleSource,
|
|
@@ -75,8 +78,9 @@ import {
|
|
|
75
78
|
serverModuleSource,
|
|
76
79
|
} from "./internal/routes.js";
|
|
77
80
|
import { TransformService, isFlowModule } from "@uniflowed/host/transform";
|
|
81
|
+
import { createChannelMiddleware } from "./internal/diagnostics.js";
|
|
78
82
|
import { send, toRequest } from "./internal/http.js";
|
|
79
|
-
import {
|
|
83
|
+
import { beginRequest } from "./internal/serve.js";
|
|
80
84
|
|
|
81
85
|
/** A resolved virtual id: Vite's convention is a leading NUL byte. */
|
|
82
86
|
const resolved = (id) => `\0${id}`;
|
|
@@ -195,7 +199,15 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
195
199
|
if (server == null) {
|
|
196
200
|
emit("rsc-split", { pages: kept.size, routes: table.routes.length });
|
|
197
201
|
}
|
|
198
|
-
|
|
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
|
+
});
|
|
199
211
|
};
|
|
200
212
|
|
|
201
213
|
/**
|
|
@@ -462,107 +474,181 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
462
474
|
}
|
|
463
475
|
|
|
464
476
|
// After Vite's own middlewares, so `/@vite/client`, `/@id/...` and
|
|
465
|
-
// static files are served first and only
|
|
466
|
-
//
|
|
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.
|
|
467
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
|
+
|
|
468
530
|
devServer.middlewares.use(async (request, response, next) => {
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
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;
|
|
477
539
|
try {
|
|
478
|
-
const url = request.url ?? "/";
|
|
479
540
|
const entry = await importServerEntry(devServer);
|
|
480
541
|
const asRequest = await toRequest(request, devServer.config);
|
|
481
542
|
|
|
482
|
-
//
|
|
483
|
-
// written
|
|
484
|
-
// `
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
// ubugeeei-prod/uf#260.
|
|
496
|
-
// 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.
|
|
497
557
|
const guarded = await entry.runMiddleware(asRequest);
|
|
498
558
|
if (guarded != null) {
|
|
499
559
|
await send(response, guarded);
|
|
500
|
-
return;
|
|
560
|
+
return true;
|
|
501
561
|
}
|
|
502
562
|
|
|
503
563
|
// Then a server action, below the guard and above the handlers.
|
|
504
|
-
// It declines
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
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.
|
|
510
571
|
const acted = await entry.callAction(asRequest);
|
|
511
572
|
if (acted != null) {
|
|
512
573
|
await send(response, acted);
|
|
513
|
-
return;
|
|
574
|
+
return true;
|
|
514
575
|
}
|
|
515
576
|
|
|
516
|
-
// Then the route handlers, above the renderer and for
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
// This step is not a duplicate of the dispatcher in `driver.js`,
|
|
523
|
-
// it is the only one that can run: this middleware is mounted by
|
|
524
|
-
// `configureServer`, which Vite calls while it is building the
|
|
525
|
-
// server, and `uf dev` adds its own after `createServer` has
|
|
526
|
-
// returned — so for every request this one claims, it is the one
|
|
527
|
-
// that decides. Without it a route handler under `uf dev` was
|
|
528
|
-
// reachable only by a client that asked for something other than
|
|
529
|
-
// HTML, and answered the 404 page to everyone else.
|
|
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.
|
|
530
583
|
const handled = await entry.dispatch(asRequest);
|
|
531
584
|
if (handled != null) {
|
|
532
585
|
await send(response, handled);
|
|
533
|
-
return;
|
|
586
|
+
return true;
|
|
534
587
|
}
|
|
535
588
|
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
// `
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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;
|
|
547
604
|
|
|
548
605
|
const result = await entry.render(
|
|
549
606
|
url,
|
|
550
607
|
{ scripts: [devUrlFor(VIRTUAL.client)], styles: [], preloads: [] },
|
|
551
|
-
{
|
|
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
|
+
},
|
|
552
615
|
);
|
|
553
616
|
if (result.error != null) reportRenderError(devServer, url, result.error);
|
|
554
|
-
// Collected rather than piped
|
|
555
|
-
//
|
|
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.
|
|
556
622
|
const html = await devServer.transformIndexHtml(url, await result.text());
|
|
557
|
-
response.statusCode = result.status;
|
|
558
|
-
|
|
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.
|
|
559
628
|
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
560
629
|
response.setHeader(name, value);
|
|
561
630
|
}
|
|
631
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
562
632
|
response.end(html);
|
|
633
|
+
return true;
|
|
563
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();
|
|
564
647
|
} catch (error) {
|
|
565
|
-
|
|
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);
|
|
566
652
|
next(error);
|
|
567
653
|
}
|
|
568
654
|
});
|
|
@@ -614,20 +700,6 @@ async function importServerEntry(devServer) {
|
|
|
614
700
|
return devServer.ssrLoadModule(VIRTUAL.server);
|
|
615
701
|
}
|
|
616
702
|
|
|
617
|
-
/**
|
|
618
|
-
* Whether this request is a server action call.
|
|
619
|
-
*
|
|
620
|
-
* The header alone, and never the path: an action is posted to the page's own
|
|
621
|
-
* URL, so there is nothing about the URL to recognise. Deliberately *not* the
|
|
622
|
-
* whole set of checks the endpoint makes — the origin, the content type, the
|
|
623
|
-
* body — because those decide whether the call is *allowed*, and a call that
|
|
624
|
-
* is not allowed must be refused by the endpoint rather than handed on to
|
|
625
|
-
* Vite's chain as though nobody had claimed it.
|
|
626
|
-
*/
|
|
627
|
-
function isActionCall(request) {
|
|
628
|
-
return request.method === "POST" && request.headers[ACTION_HEADER] != null;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
703
|
function wantsDocument(request) {
|
|
632
704
|
if (request.method !== "GET" && request.method !== "HEAD") return false;
|
|
633
705
|
const url = request.url ?? "/";
|
|
@@ -647,14 +719,25 @@ const ALL_DIAGNOSTICS = "UF_REACT_COMPILER_DIAGNOSTICS";
|
|
|
647
719
|
* Report what the React Compiler said about one module.
|
|
648
720
|
*
|
|
649
721
|
* Every finding used to be printed as `a function: <message>` — no file, no
|
|
650
|
-
* line, no column, and the fallback string doing all the work because
|
|
651
|
-
*
|
|
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
|
|
652
726
|
* knows the module and the compiler gives a position for most findings, so
|
|
653
727
|
* both go into the message: Vite prints a plugin warning's `message` and
|
|
654
728
|
* nothing else, so a location that is not in the string is a location the
|
|
655
729
|
* reader never sees. `id` and `loc` go along for anything reading the log
|
|
656
730
|
* object rather than the line. See ubugeeei-prod/uf#307.
|
|
657
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
|
+
*
|
|
658
741
|
* A dependency's findings are held back. A React Compiler bailout inside
|
|
659
742
|
* `@uniflowed/form` is not something the person running the build can fix, and
|
|
660
743
|
* a channel carrying forty of them on every build is a channel people stop
|
|
@@ -677,9 +760,20 @@ function reportDiagnostics(context, { id, root, diagnostics, environment, report
|
|
|
677
760
|
const mine = isProjectModule(root, id) || process.env[ALL_DIAGNOSTICS] === "all";
|
|
678
761
|
for (const diagnostic of diagnostics) {
|
|
679
762
|
// Everything a reader would be shown, so two findings that would print as
|
|
680
|
-
// the same line collapse into one.
|
|
681
|
-
//
|
|
682
|
-
//
|
|
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.
|
|
683
777
|
const signature = `${diagnostic.kind}\0${diagnostic.line}\0${diagnostic.column}\0${diagnostic.message}`;
|
|
684
778
|
if (ledger.signatures.has(signature)) continue;
|
|
685
779
|
ledger.signatures.add(signature);
|