@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 +1 -1
- package/src/client-deps.js +25 -0
- package/src/dev-app.js +18 -0
- package/src/dev-server.js +8 -2
- package/src/head.js +16 -2
- package/src/render-session.js +11 -3
- package/src/stylex-dev.js +12 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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));
|
package/src/render-session.js
CHANGED
|
@@ -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
|
-
|
|
355
|
-
|
|
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
|
}
|