@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/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 renders every HTML request on the
18
- // server, so `uf dev` serves the same markup `uf build` writes.
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 { withRequest } from "./internal/serve.js";
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
- return routesModuleSource(table, { shipsPage: (route) => kept.has(route) });
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 a document request reaches
466
- // the renderer.
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
- // Two kinds of request reach uf here, and the second one is why this
470
- // is not `wantsDocument` alone: a server action is a `POST` carrying
471
- // `uf-action`, which every gate below the renderer would refuse.
472
- // `driver.js` claims every request and can afford to decide later;
473
- // this middleware is mounted behind Vite's own and has to say up
474
- // front which ones are uf's.
475
- const document = wantsDocument(request);
476
- if (!document && !isActionCall(request)) return next();
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
- // One request, owned here and settled once the document has been
483
- // written — the same lifecycle `driver.js` gives `uf dev` and
484
- // `internal/serve.js` gives `uf preview` and `uf start`. A project
485
- // driving Vite itself must not get a different answer about when
486
- // `after()` runs than the same project run through `uf dev`; see
487
- // `internal/serve.js` and ubugeeei-prod/uf#389.
488
- //
489
- // Only requests that look like a document reach here, so unlike
490
- // `driver.js` there is no path where uf hands the response back to
491
- // Vite's chain: what is below either writes it or throws.
492
- await withRequest(entry, asRequest, async () => {
493
- // Before anything answers: a middleware guards a subtree, and a
494
- // page rendered while the guard on it had not run is the whole of
495
- // ubugeeei-prod/uf#260. `driver.js` makes the same call, for
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 anything that carries no action id, so the two
505
- // lines cost a document request one `headers.get`; and it never
506
- // declines one that does, so an action call cannot reach a route
507
- // handler that happens to share the URL it was posted to. The
508
- // same two lines are in `driver.js`, in `fetch.js` for every
509
- // deploy adapter, and in `standalone.js`.
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 the same
517
- // reason `driver.js` puts them there: a path that answers a
518
- // request is not a document, whatever the client said it would
519
- // accept. `curl /api/thing` and a `<form action>` navigation both
520
- // send `Accept: text/html`, and both want the handler's answer.
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
- // A `POST` this middleware claimed because it named an action,
537
- // that the endpoint then declined and no handler answered. It
538
- // cannot happen — the endpoint answers every request carrying an
539
- // id, including every refusal — and a page cannot answer a
540
- // `POST` anyway, so the honest end is a 404 rather than a
541
- // rendered document with a 200.
542
- if (!document) {
543
- response.statusCode = 404;
544
- response.end();
545
- return;
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
- { onError: (error) => reportRenderError(devServer, url, error) },
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, for the reason `driver.js` gives at
555
- // step 4: `transformIndexHtml` is a whole-document hook.
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
- response.setHeader("Content-Type", "text/html; charset=utf-8");
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
- devServer.ssrFixStacktrace(error);
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 the
651
- * compiler names an inner function about as often as not. The transform hook
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. The compiler reports "Cannot access refs
681
- // during render" once per pass that noticed it — three times for one `ref`
682
- // — and three identical lines are not three things to fix.
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);