@tinvar/renderer 0.4.2 → 0.4.6
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/dev-app.js +18 -0
- package/src/dev-server.js +7 -1
- package/src/head.js +16 -2
- package/src/panel-styles.js +3 -2
- package/src/render-session.js +11 -3
- package/src/stylex-dev.js +171 -0
package/package.json
CHANGED
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 { styleXDevStyles } from './stylex-dev.js';
|
|
35
36
|
export { assertDevelopment, DevServerRefused, PRODUCTION_MARKERS } from './dev-guard.js';
|
|
36
37
|
|
|
37
38
|
/** Where the dev server's modules are served from, behind PHP's server. */
|
|
@@ -88,6 +89,9 @@ export async function createDevServer({
|
|
|
88
89
|
let listened = null;
|
|
89
90
|
const restartListening = () => (listened ??= Promise.withResolvers()).promise;
|
|
90
91
|
|
|
92
|
+
// StyleX's rules for pages in dev (ADR 0179): served under the base,
|
|
93
|
+
// taken in by the client entry, and linked from each document's head.
|
|
94
|
+
const styles = styleXDevStyles(DEV_BASE);
|
|
91
95
|
const vite = await createServer({
|
|
92
96
|
root: appRoot,
|
|
93
97
|
configFile: configFileOf(appRoot),
|
|
@@ -97,6 +101,7 @@ export async function createDevServer({
|
|
|
97
101
|
clearScreen: false,
|
|
98
102
|
...(logLevel ? { logLevel } : {}),
|
|
99
103
|
plugins: [
|
|
104
|
+
styles,
|
|
100
105
|
ignoreReplayedChanges(watchedFrom),
|
|
101
106
|
followRestarts(() => {
|
|
102
107
|
const waiting = listened;
|
|
@@ -196,6 +201,7 @@ export async function createDevServer({
|
|
|
196
201
|
throw error;
|
|
197
202
|
}
|
|
198
203
|
useDevEntry(load);
|
|
204
|
+
useDevHead(() => styles.api.link());
|
|
199
205
|
|
|
200
206
|
// Loaded once now, so a broken entry fails the start rather than the
|
|
201
207
|
// 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/panel-styles.js
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
// one of nothing, put a page's queries in two frames instead of one, React
|
|
25
25
|
// scheduling its render around the stylesheet. A panel's page links its
|
|
26
26
|
// sheet through `virtual:tinvar/styles`, read from the RSC plugin's assets
|
|
27
|
-
// manifest, which the client build fills in; in dev
|
|
28
|
-
//
|
|
27
|
+
// manifest, which the client build fills in; in dev the client entry takes
|
|
28
|
+
// what StyleX has compiled into the page (stylex-dev.js), and the component
|
|
29
|
+
// renders nothing.
|
|
29
30
|
|
|
30
31
|
import { createRequire } from 'node:module';
|
|
31
32
|
import { join } from 'node:path';
|
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
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// StyleX's rules in a page rendered in dev (ADR 0179).
|
|
2
|
+
//
|
|
3
|
+
// StyleX's Vite plugin compiles each stylex.create() as Vite transforms the
|
|
4
|
+
// module, and in dev hands the browser what it has compiled in two ways:
|
|
5
|
+
// a stylesheet link and a runtime script, both added through Vite's
|
|
6
|
+
// transformIndexHtml. A page here is rendered by the app's rsc and ssr
|
|
7
|
+
// entries, for PHP, and never goes through index.html, so neither was ever
|
|
8
|
+
// added. And the plugin serves that stylesheet at /virtual:stylex.css, off
|
|
9
|
+
// Vite's base, where PHP answers. So no page had a rule of the kit's or the
|
|
10
|
+
// app's in dev, though every element carried its classes.
|
|
11
|
+
//
|
|
12
|
+
// This serves what StyleX has compiled under the dev server's base, which
|
|
13
|
+
// the dev Caddyfile already passes to Vite, and adds to the client entry a
|
|
14
|
+
// module that puts it in a <style> of its own and fetches it again after
|
|
15
|
+
// every update Vite applies, as StyleX's runtime would. The page's policy
|
|
16
|
+
// governs scripts alone, so the element is allowed.
|
|
17
|
+
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { normalizePath } from 'vite';
|
|
20
|
+
|
|
21
|
+
/** Where the compiled rules are served, under the dev server's base. */
|
|
22
|
+
export const STYLEX_DEV_CSS = '__tinvar/stylex.css';
|
|
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
|
+
|
|
27
|
+
/** The module the client entry imports to take the rules into the page. */
|
|
28
|
+
export const STYLEX_DEV_MODULE = 'virtual:tinvar/stylex-dev';
|
|
29
|
+
|
|
30
|
+
const RESOLVED = `\0${STYLEX_DEV_MODULE}`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The plugin the dev server adds to the app's own.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} base the dev server's base, `/_vite/`
|
|
36
|
+
* @returns {import('vite').Plugin}
|
|
37
|
+
*/
|
|
38
|
+
export function styleXDevStyles(base) {
|
|
39
|
+
/** @type {null | (() => string)} */
|
|
40
|
+
let collect = null;
|
|
41
|
+
/** @type {string[]} the client build's inputs, as its config names them */
|
|
42
|
+
let inputs = [];
|
|
43
|
+
/** @type {string} */
|
|
44
|
+
let root = '';
|
|
45
|
+
/** @type {Promise<Set<string>> | null} the ids Vite resolves them to, once asked */
|
|
46
|
+
let entries = null;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name: 'tinvar:stylex-dev',
|
|
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
|
+
},
|
|
56
|
+
|
|
57
|
+
configResolved(config) {
|
|
58
|
+
// The app's StyleX plugin: the one that can say what it compiled.
|
|
59
|
+
const styleX = config.plugins.find(
|
|
60
|
+
(plugin) => typeof (/** @type {any} */ (plugin).__stylexCollectCss) === 'function',
|
|
61
|
+
);
|
|
62
|
+
collect = styleX ? () => /** @type {any} */ (styleX).__stylexCollectCss() ?? '' : null;
|
|
63
|
+
inputs = clientInputs(config);
|
|
64
|
+
root = config.root;
|
|
65
|
+
entries = null;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
configureServer(server) {
|
|
69
|
+
if (collect === null) return;
|
|
70
|
+
const served = `${base}${STYLEX_DEV_CSS}`;
|
|
71
|
+
server.middlewares.use((req, res, next) => {
|
|
72
|
+
const url = (req.url ?? '').replace(/[?#].*$/, '');
|
|
73
|
+
if (url !== served) return next();
|
|
74
|
+
res.statusCode = 200;
|
|
75
|
+
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
76
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
77
|
+
res.end(collect?.() ?? '');
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
resolveId(id) {
|
|
82
|
+
return collect !== null && id === STYLEX_DEV_MODULE ? RESOLVED : null;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
load(id) {
|
|
86
|
+
if (id !== RESOLVED) return null;
|
|
87
|
+
return styleXDevRuntime(`${base}${STYLEX_DEV_CSS}`);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async transform(code, id) {
|
|
91
|
+
if (collect === null || this.environment?.name !== 'client') return null;
|
|
92
|
+
// Which modules the entries are is Vite's to say: an input may name
|
|
93
|
+
// one without its extension, or through an alias. Resolved once, as
|
|
94
|
+
// an import from the app's root would be.
|
|
95
|
+
const importer = path.join(root, 'index.html');
|
|
96
|
+
entries ??= Promise.all(inputs.map((input) => this.resolve(input, importer))).then(
|
|
97
|
+
(resolved) =>
|
|
98
|
+
new Set(resolved.filter((r) => r != null).map((r) => normalizePath(r.id.replace(/[?#].*$/, '')))),
|
|
99
|
+
);
|
|
100
|
+
if (!(await entries).has(normalizePath(id.replace(/[?#].*$/, '')))) return null;
|
|
101
|
+
// On the entry's first line: its source map's lines stay where they were.
|
|
102
|
+
return { code: `import ${JSON.stringify(STYLEX_DEV_MODULE)};${code}`, map: null };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The client environment's entries, as its build's rollup input names them.
|
|
109
|
+
*
|
|
110
|
+
* @param {import('vite').ResolvedConfig} config
|
|
111
|
+
* @returns {string[]}
|
|
112
|
+
*/
|
|
113
|
+
function clientInputs(config) {
|
|
114
|
+
const input = /** @type {any} */ (config).environments?.client?.build?.rollupOptions?.input;
|
|
115
|
+
const named =
|
|
116
|
+
typeof input === 'string'
|
|
117
|
+
? [input]
|
|
118
|
+
: Array.isArray(input)
|
|
119
|
+
? input
|
|
120
|
+
: input && typeof input === 'object'
|
|
121
|
+
? Object.values(input)
|
|
122
|
+
: [];
|
|
123
|
+
return named.filter((file) => typeof file === 'string');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The module the browser runs: the rules in a <style> of their own, fetched
|
|
128
|
+
* again after every update Vite applies, since an edit compiles new ones.
|
|
129
|
+
* Only the newest request's answer is shown: one edit sets off more than
|
|
130
|
+
* one fetch, and an older answer arriving last would put back rules from
|
|
131
|
+
* before it.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} url
|
|
134
|
+
*/
|
|
135
|
+
export function styleXDevRuntime(url) {
|
|
136
|
+
return `const URL = ${JSON.stringify(url)};
|
|
137
|
+
const LINK = ${JSON.stringify(STYLEX_DEV_LINK)};
|
|
138
|
+
const ID = 'tinvar-stylex-dev';
|
|
139
|
+
let last = null;
|
|
140
|
+
let asked = 0;
|
|
141
|
+
function element() {
|
|
142
|
+
let el = document.getElementById(ID);
|
|
143
|
+
if (!el) {
|
|
144
|
+
el = document.createElement('style');
|
|
145
|
+
el.id = ID;
|
|
146
|
+
document.head.appendChild(el);
|
|
147
|
+
}
|
|
148
|
+
return el;
|
|
149
|
+
}
|
|
150
|
+
async function update() {
|
|
151
|
+
const request = ++asked;
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(URL + '?t=' + Date.now(), { cache: 'no-store' });
|
|
154
|
+
if (!response.ok || request !== asked) return;
|
|
155
|
+
const css = await response.text();
|
|
156
|
+
if (request === asked && css !== last) {
|
|
157
|
+
element().textContent = css;
|
|
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;
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
update();
|
|
166
|
+
if (import.meta.hot) {
|
|
167
|
+
import.meta.hot.on('stylex:css-update', update);
|
|
168
|
+
import.meta.hot.on('vite:afterUpdate', () => setTimeout(update, 180));
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
}
|