@uniflowed/vite 0.0.0-alpha.18 → 0.0.0-alpha.20

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
@@ -135,6 +135,20 @@ export default function uniflowed(options = {}) {
135
135
  // no `uf.config.js` will mention. It only ever reaches the *development*
136
136
  // client entry; see `flowPlugin`'s `load`. ubugeeei-prod/uf#516.
137
137
  const strictMode = app.react?.strictMode !== false;
138
+ // What the browser does with a link, read here for the reason Strict Mode is
139
+ // and honoured in every command rather than in the build alone: it is the
140
+ // one setting whose whole effect is what happens on a click, so a dev server
141
+ // that disagreed with the deployment would be the wrong application to look
142
+ // at. Anything but `"document"` is the client router, which is what every
143
+ // project that has not heard of the key has.
144
+ const navigation = app.rendering?.navigation === "document" ? "document" : "client";
145
+ // Whether this application starts by attaching to markup or by rendering
146
+ // into an empty root. `["csr"]` is the only list that means the second, and
147
+ // `uf` refuses that value beside any other while the config is read — so the
148
+ // question here is "is it in the list", not "is it the only thing in it",
149
+ // and a driver started by hand on a config `uf` never validated gets the same
150
+ // answer for the same reason a project would want.
151
+ const mount = (app.rendering?.modes ?? []).includes("csr") ? "render" : "hydrate";
138
152
 
139
153
  const accessibility = ufConfig.accessibility ?? {};
140
154
 
@@ -143,6 +157,8 @@ export default function uniflowed(options = {}) {
143
157
  routerRoot,
144
158
  appEntry,
145
159
  strictMode,
160
+ navigation,
161
+ mount,
146
162
  command: options.command,
147
163
  accessibility,
148
164
  }),
@@ -157,7 +173,15 @@ export default function uniflowed(options = {}) {
157
173
  ];
158
174
  }
159
175
 
160
- function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }) {
176
+ function flowPlugin({
177
+ routerRoot,
178
+ appEntry,
179
+ strictMode,
180
+ navigation,
181
+ mount,
182
+ command,
183
+ accessibility,
184
+ }) {
161
185
  let root = process.cwd();
162
186
  let isProduction = false;
163
187
  /**
@@ -331,7 +355,10 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
331
355
  ensureService();
332
356
  },
333
357
 
334
- resolveId(id) {
358
+ resolveId(id, importer, resolveOptions) {
359
+ if (id === "@uniflowed/react" && !isSsr(this, resolveOptions)) {
360
+ return this.resolve("react", importer, { ...resolveOptions, skipSelf: true });
361
+ }
335
362
  if (id === RUNTIME_PUBLIC_PATH) return RUNTIME_RESOLVED_ID;
336
363
  if (id === AUDIT_PUBLIC_PATH) return AUDIT_RESOLVED_ID;
337
364
  if (VIRTUAL_IDS.has(id)) return resolved(id);
@@ -356,8 +383,17 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
356
383
  // Strict Mode belongs to the client entry and to development only: a
357
384
  // build passes `false`, so the generated module is the one that existed
358
385
  // before #516 and a visitor's browser renders once.
386
+ //
387
+ // `navigation` is in the same entry and has no `isProduction` beside it,
388
+ // deliberately: it is what a link does, and a dev server whose links
389
+ // behave differently from the deployment is the wrong thing to be
390
+ // looking at. See `clientModuleSource`.
359
391
  if (id === resolved(VIRTUAL.client)) {
360
- return clientModuleSource(entryPath, { strictMode: strictMode && !isProduction });
392
+ return clientModuleSource(entryPath, {
393
+ strictMode: strictMode && !isProduction,
394
+ navigation,
395
+ mount,
396
+ });
361
397
  }
362
398
  if (id === resolved(VIRTUAL.server)) return serverModuleSource(entryPath);
363
399
  // Only `virtual:uf/server` imports this, so it is only ever asked for in
@@ -474,12 +510,13 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
474
510
  const tags = [
475
511
  {
476
512
  tag: "script",
513
+ attrs: { "data-uf-dev-head-preamble": "react-devtools" },
477
514
  children: devtoolsPreamble(),
478
515
  injectTo: "head-prepend",
479
516
  },
480
517
  {
481
518
  tag: "script",
482
- attrs: { type: "module" },
519
+ attrs: { type: "module", "data-uf-dev-head-preamble": "react-refresh" },
483
520
  children: preambleCode(base),
484
521
  injectTo: "head-prepend",
485
522
  },
@@ -509,9 +546,8 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
509
546
  // missing `route` — so adding a route handler to a running dev server
510
547
  // did not rebuild the table and the handler stayed invisible until a
511
548
  // restart. A list that has to match another list has to be that list.
512
- const stems = Object.values(RESERVED)
513
- .map((stem) => stem.replaceAll(".", "\\."))
514
- .join("|");
549
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
550
+ const stems = Object.values(RESERVED).map(escapeRegExp).join("|");
515
551
  const reserved = new RegExp(`/(${stems})(\\.[a-z]+)?\\.(js|jsx|mdx)$`);
516
552
  const onRouteFile = (file) => {
517
553
  if (!reserved.test(file) || !file.startsWith(appRoot)) return;
@@ -562,7 +598,7 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
562
598
  // claimed, this one decided, and the other was reached only for what
563
599
  // this one declined. Route-handler dispatch was in the other. A
564
600
  // `GET /feed` from a browser is `Accept: text/html` with no extension,
565
- // so it looked like a document, so `app/feed/_uf.route.js` was never
601
+ // so it looked like a document, so `app/feed/$route.js` was never
566
602
  // asked and the reader got the route table's page — or the not-found
567
603
  // page — for a path that had a handler. See ubugeeei-prod/uf#349.
568
604
  //
@@ -598,7 +634,7 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
598
634
  // list rather than something this middleware can decide on its own.
599
635
  return () => {
600
636
  // The browser's own reporting channel, mounted above the application
601
- // so that a report never reaches a project's `_uf.middleware.js` or
637
+ // so that a report never reaches a project's `$middleware.js` or
602
638
  // its route table. See `internal/diagnostics.js`.
603
639
  devServer.middlewares.use(
604
640
  createChannelMiddleware((diagnostic) => emit("diagnostic", diagnostic)),
@@ -687,6 +723,30 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
687
723
  }
688
724
  if (notDocument != null) return false;
689
725
 
726
+ // A single-page project's deployment answers every navigation
727
+ // with the same empty shell, so this does too. Rendering the
728
+ // route here instead would have been the better-looking dev
729
+ // server and the wrong one: a page that only works because the
730
+ // server rendered it would work all through development and be
731
+ // blank the day it shipped. It is the same argument
732
+ // `app.rendering.navigation` makes about a link, one level up.
733
+ //
734
+ // The three steps above still ran — the guard, the action, the
735
+ // handler — and each of them is something `uf build` refuses in
736
+ // a `["csr"]` project by name. A dev server that skipped them
737
+ // would hide the very thing the build is going to stop.
738
+ if (mount === "render") {
739
+ response.statusCode = 200;
740
+ response.setHeader("content-type", "text/html; charset=utf-8");
741
+ const shell = entry.shellDocument({
742
+ scripts: [devUrlFor(VIRTUAL.client)],
743
+ styles: [],
744
+ preloads: [],
745
+ });
746
+ response.end(await devServer.transformIndexHtml(url, shell));
747
+ return true;
748
+ }
749
+
690
750
  const result = await entry.render(
691
751
  url,
692
752
  { scripts: [devUrlFor(VIRTUAL.client)], styles: [], preloads: [] },
@@ -699,6 +759,22 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
699
759
  // Vite sees the head and only the head. That is what lets the
700
760
  // development server stream like every other host — see below.
701
761
  transformHead: (head) => devServer.transformIndexHtml(url, head),
762
+ // And what the streaming actually did, when it changed. The
763
+ // router has already decided there is something worth saying
764
+ // and written the words — see its `internal/inspector.js`,
765
+ // which cannot be imported from here because this file is
766
+ // plain JavaScript that Vite loads before any Flow transform
767
+ // exists. `info`, because a page that streamed is a
768
+ // measurement and not a problem; `origin`, because a report
769
+ // that does not say which page produced it is one somebody
770
+ // has to reproduce before they can act on it.
771
+ onStream: (diagnostic) =>
772
+ emit("diagnostic", {
773
+ severity: "info",
774
+ origin: url,
775
+ message: diagnostic.message,
776
+ detail: diagnostic.detail,
777
+ }),
702
778
  },
703
779
  );
704
780
  if (result.error != null) reportRenderError(devServer, url, result.error);
@@ -707,7 +783,7 @@ function flowPlugin({ routerRoot, appEntry, strictMode, command, accessibility }
707
783
  // `transformIndexHtml` is a *whole document* hook — which made the
708
784
  // one place a developer would notice streaming the one place it
709
785
  // did not happen: a slow page showed nothing until it was finished
710
- // and `_uf.loading.js` looked broken.
786
+ // and `$loading.js` looked broken.
711
787
  //
712
788
  // `transformHead` above is the seam. `internal/stream.js` already
713
789
  // held the opening chunk back until the head was complete and
package/internal/a11y.js CHANGED
@@ -86,15 +86,17 @@ export function auditRuntimeSource(settings) {
86
86
  /**
87
87
  * The tag that loads it, or `null` when this project has no engine.
88
88
  *
89
- * `injectTo: "body"` rather than the head: the audit reads the rendered tree,
90
- * so there is nothing for it to do until there is one, and a script in the
91
- * head would only sit through the same wait with the parser stopped behind it.
89
+ * `injectTo: "head"` because `uf dev` streams the body. Vite's HTML hook only
90
+ * sees the opening chunk; a `body` injection can land at a React chunk boundary
91
+ * and split an attribute before the rest of the body arrives. A module in the
92
+ * head still waits for a settled DOM before auditing — see the runtime — while
93
+ * staying in markup the opening transform can safely rewrite.
92
94
  */
93
95
  export function auditTag(base, available) {
94
96
  if (!available) return null;
95
97
  return {
96
98
  tag: "script",
97
99
  attrs: { type: "module", src: `${base}${AUDIT_PUBLIC_PATH.slice(1)}` },
98
- injectTo: "body",
100
+ injectTo: "head",
99
101
  };
100
102
  }
@@ -23,8 +23,8 @@
23
23
  // how the preamble is injected, could have taken it away in a diff nobody would
24
24
  // read as being about DevTools. See ubugeeei-prod/uf#503.
25
25
  //
26
- // So the hook is installed here, first, deliberately, and `tests/library/
27
- // devtools.test.js` runs this script's own text against a fake window.
26
+ // So the hook is installed here, first, deliberately, and `devtools.test.js`
27
+ // beside this package runs this script's own text against a fake window.
28
28
  //
29
29
  // # Three things DevTools needs, and what carries each
30
30
  //
@@ -57,7 +57,7 @@
57
57
  // `@uniflowed/router`'s `internal/diagnostics.js` are the same two strings, and
58
58
  // they are the contract. They cannot be *imported* here: this module is loaded
59
59
  // by Vite before any Flow transform exists, and both of those are Flow. So they
60
- // are written out, and `tests/library/dev-channel.test.js` asserts that all
60
+ // are written out, and `packages/vite/dev-channel.test.js` asserts that all
61
61
  // four spellings agree — a duplicated constant with a test on it is honest, and
62
62
  // one without is how the browser ends up posting to a path nothing serves.
63
63
  //
@@ -105,7 +105,7 @@ const SEVERITIES = new Set(["error", "warn", "info"]);
105
105
  * The connect middleware that answers the channel.
106
106
  *
107
107
  * Mounted **before** the application middleware, so a request under `/__uf/`
108
- * never reaches a project's `_uf.middleware.js` or its route table. A guard
108
+ * never reaches a project's `$middleware.js` or its route table. A guard
109
109
  * that ran for a page's own telemetry would be a guard asked a question the
110
110
  * application never asks, and one that redirected it would turn a report into
111
111
  * a login page.
@@ -435,7 +435,7 @@ export function markLines(lines) {
435
435
  *
436
436
  * Exported because it is where the decision is visible without starting Shiki:
437
437
  * give it the token split a grammar would produce and it says which words it
438
- * marked. `tests/library/highlight.test.js` uses exactly that, and the splits
438
+ * marked. `packages/vite/highlight.test.js` uses exactly that, and the splits
439
439
  * in it are the ones that had bugs.
440
440
  */
441
441
  export function markLine(line) {