@tinvar/renderer 0.4.5 → 0.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinvar/renderer",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
@@ -0,0 +1,25 @@
1
+ // What the browser's dependency optimizer leaves alone in dev (ADR 0180).
2
+ //
3
+ // Vite bundles the packages the browser imports before it serves them.
4
+ // @tinvar/react's client components are client references, which the RSC
5
+ // plugin points the browser at by their own path; bundled into the
6
+ // browser's @tinvar/react as well, a page loaded both copies, each with its
7
+ // own React, and the first page of every new app failed with "Invalid hook
8
+ // call" until Vite reloaded it. The plugin says so, as "client component
9
+ // dependency is inconsistently optimized". Served as its own modules, the
10
+ // package loads once. What it imports, React alone, is optimized as before.
11
+
12
+ /** Served to the browser as their own modules, never optimized into a bundle. */
13
+ export const NOT_OPTIMIZED = ['@tinvar/react'];
14
+
15
+ /** @returns {import('vite').Plugin} */
16
+ export function clientDependencies() {
17
+ return {
18
+ name: 'tinvar:client-dependencies',
19
+ apply: 'serve',
20
+ configEnvironment(name) {
21
+ // Merged with the app's own: Vite concatenates the lists.
22
+ return name === 'client' ? { optimizeDeps: { exclude: [...NOT_OPTIMIZED] } } : undefined;
23
+ },
24
+ };
25
+ }
package/src/dev-app.js CHANGED
@@ -15,6 +15,24 @@ export function useDevEntry(loader) {
15
15
  load = loader;
16
16
  }
17
17
 
18
+ /** @type {() => string} */
19
+ let head = () => '';
20
+
21
+ /** Sets what the dev server adds to every document's head (ADR 0179). */
22
+ export function useDevHead(markup) {
23
+ head = markup;
24
+ }
25
+
26
+ /**
27
+ * What the renderer adds before each document's `</head>` in dev: the link
28
+ * to StyleX's dev stylesheet, so a page is styled from its first paint
29
+ * rather than once its script has run. Asked at each render, since a
30
+ * restarted dev server may have StyleX where it had none.
31
+ */
32
+ export function tinvarDevHead() {
33
+ return head();
34
+ }
35
+
18
36
  async function entry() {
19
37
  if (load === null) throw new Error('The dev entry was imported before the dev server started.');
20
38
  return load();
package/src/dev-server.js CHANGED
@@ -19,7 +19,7 @@
19
19
  import { createRequire } from 'node:module';
20
20
  import { existsSync, mkdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
21
21
  import path from 'node:path';
22
- import { useDevEntry } from './dev-app.js';
22
+ import { useDevEntry, useDevHead } from './dev-app.js';
23
23
  import { assertDevelopment, DevServerRefused } from './dev-guard.js';
24
24
  import {
25
25
  actionsOf,
@@ -32,6 +32,7 @@ import {
32
32
  viewsOf,
33
33
  } from './vite.js';
34
34
 
35
+ import { clientDependencies } from './client-deps.js';
35
36
  import { styleXDevStyles } from './stylex-dev.js';
36
37
  export { assertDevelopment, DevServerRefused, PRODUCTION_MARKERS } from './dev-guard.js';
37
38
 
@@ -89,6 +90,9 @@ export async function createDevServer({
89
90
  let listened = null;
90
91
  const restartListening = () => (listened ??= Promise.withResolvers()).promise;
91
92
 
93
+ // StyleX's rules for pages in dev (ADR 0179): served under the base,
94
+ // taken in by the client entry, and linked from each document's head.
95
+ const styles = styleXDevStyles(DEV_BASE);
92
96
  const vite = await createServer({
93
97
  root: appRoot,
94
98
  configFile: configFileOf(appRoot),
@@ -98,7 +102,8 @@ export async function createDevServer({
98
102
  clearScreen: false,
99
103
  ...(logLevel ? { logLevel } : {}),
100
104
  plugins: [
101
- styleXDevStyles(DEV_BASE),
105
+ styles,
106
+ clientDependencies(),
102
107
  ignoreReplayedChanges(watchedFrom),
103
108
  followRestarts(() => {
104
109
  const waiting = listened;
@@ -198,6 +203,7 @@ export async function createDevServer({
198
203
  throw error;
199
204
  }
200
205
  useDevEntry(load);
206
+ useDevHead(() => styles.api.link());
201
207
 
202
208
  // Loaded once now, so a broken entry fails the start rather than the
203
209
  // first page, and the module graph the deferred manifest is read from
package/src/head.js CHANGED
@@ -448,6 +448,18 @@ export function modulePreloads(document) {
448
448
  return [...new Set(out)];
449
449
  }
450
450
 
451
+ /**
452
+ * $document, which ends with `</head>`, with $extra just before it.
453
+ *
454
+ * @param {string} document
455
+ * @param {string} extra
456
+ */
457
+ function withExtra(document, extra) {
458
+ if (extra === '') return document;
459
+ const close = asciiLower(document).lastIndexOf('</head>');
460
+ return close === -1 ? document : document.slice(0, close) + extra + document.slice(close);
461
+ }
462
+
451
463
  /**
452
464
  * The HTML stream with its head resolved: held until `</head>` arrives,
453
465
  * then sent with the fallbacks written and the duplicates dropped, and
@@ -457,9 +469,11 @@ export function modulePreloads(document) {
457
469
  * @param {unknown} head the render frame's `head`
458
470
  * @param {(resolved: { preload: string[] }) => void} [onResolved] told, once,
459
471
  * what the resolved head preloads -- for PHP's Link header
472
+ * @param {string} [extra] markup the renderer adds before `</head>`, its own
473
+ * and never a frame's: in dev, the link to StyleX's stylesheet (ADR 0179)
460
474
  * @returns {ReadableStream<Uint8Array>}
461
475
  */
462
- export function withHead(html, head, onResolved) {
476
+ export function withHead(html, head, onResolved, extra = '') {
463
477
  const fallbacks = fallbackTags(head);
464
478
  // What is held, as it came: joined once, when `</head>` is found, and
465
479
  // searched only where the newest chunk could finish it -- copying and
@@ -510,7 +524,7 @@ export function withHead(html, head, onResolved) {
510
524
 
511
525
  const bytes = join();
512
526
  const end = windowStart + found;
513
- const resolvedHead = resolveHead(decoder.decode(bytes.subarray(0, end)), fallbacks);
527
+ const resolvedHead = withExtra(resolveHead(decoder.decode(bytes.subarray(0, end)), fallbacks), extra);
514
528
  onResolved?.({ preload: modulePreloads(resolvedHead) });
515
529
  controller.enqueue(encoder.encode(resolvedHead));
516
530
  if (end < bytes.length) controller.enqueue(bytes.subarray(end));
@@ -351,9 +351,17 @@ export class RenderSession {
351
351
  // among them until a component's replace them. What the head preloads
352
352
  // goes to PHP as a `meta` frame -- the page's client references, for its
353
353
  // Link header -- before the first byte of the document does.
354
- htmlStream = withHead(htmlStream, frame.head, ({ preload }) => {
355
- if (preload.length > 0) this.#send({ t: FRAME.META, preload });
356
- });
354
+ // The dev renderer's app adds its own to the head -- the link to
355
+ // StyleX's dev stylesheet (ADR 0179); a built app's entry, nothing.
356
+ const devHead = typeof this.#app.tinvarDevHead === 'function' ? String(this.#app.tinvarDevHead()) : '';
357
+ htmlStream = withHead(
358
+ htmlStream,
359
+ frame.head,
360
+ ({ preload }) => {
361
+ if (preload.length > 0) this.#send({ t: FRAME.META, preload });
362
+ },
363
+ devHead,
364
+ );
357
365
  return injectRscPayload(htmlStream, rscStream, frame.nonce, frame.page);
358
366
  } catch (error) {
359
367
  // Both renders are running by now, and neither has a reader: left
package/src/stylex-dev.js CHANGED
@@ -21,6 +21,9 @@ import { normalizePath } from 'vite';
21
21
  /** Where the compiled rules are served, under the dev server's base. */
22
22
  export const STYLEX_DEV_CSS = '__tinvar/stylex.css';
23
23
 
24
+ /** The attribute that marks the head's link, which the runtime disables once it has the rules. */
25
+ export const STYLEX_DEV_LINK = 'data-tinvar-stylex-dev';
26
+
24
27
  /** The module the client entry imports to take the rules into the page. */
25
28
  export const STYLEX_DEV_MODULE = 'virtual:tinvar/stylex-dev';
26
29
 
@@ -45,6 +48,11 @@ export function styleXDevStyles(base) {
45
48
  return {
46
49
  name: 'tinvar:stylex-dev',
47
50
  apply: 'serve',
51
+ api: {
52
+ /** The link that styles a page from its first paint, for its head (ADR 0179). */
53
+ link: () =>
54
+ collect === null ? '' : `<link rel="stylesheet" href="${base}${STYLEX_DEV_CSS}" ${STYLEX_DEV_LINK}>`,
55
+ },
48
56
 
49
57
  configResolved(config) {
50
58
  // The app's StyleX plugin: the one that can say what it compiled.
@@ -126,6 +134,7 @@ function clientInputs(config) {
126
134
  */
127
135
  export function styleXDevRuntime(url) {
128
136
  return `const URL = ${JSON.stringify(url)};
137
+ const LINK = ${JSON.stringify(STYLEX_DEV_LINK)};
129
138
  const ID = 'tinvar-stylex-dev';
130
139
  let last = null;
131
140
  let asked = 0;
@@ -147,6 +156,9 @@ async function update() {
147
156
  if (request === asked && css !== last) {
148
157
  element().textContent = css;
149
158
  last = css;
159
+ // The head's link styled the first paint; from here the <style> is
160
+ // current, and the link would keep rules an edit took out.
161
+ for (const link of document.querySelectorAll('link[' + LINK + ']')) link.disabled = true;
150
162
  }
151
163
  } catch {}
152
164
  }