@tinvar/renderer 0.4.2 → 0.4.5

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.2",
3
+ "version": "0.4.5",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
package/src/dev-server.js CHANGED
@@ -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. */
@@ -97,6 +98,7 @@ export async function createDevServer({
97
98
  clearScreen: false,
98
99
  ...(logLevel ? { logLevel } : {}),
99
100
  plugins: [
101
+ styleXDevStyles(DEV_BASE),
100
102
  ignoreReplayedChanges(watchedFrom),
101
103
  followRestarts(() => {
102
104
  const waiting = listened;
@@ -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 StyleX hands the browser
28
- // its CSS itself, and the component renders nothing.
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';
@@ -0,0 +1,159 @@
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 module the client entry imports to take the rules into the page. */
25
+ export const STYLEX_DEV_MODULE = 'virtual:tinvar/stylex-dev';
26
+
27
+ const RESOLVED = `\0${STYLEX_DEV_MODULE}`;
28
+
29
+ /**
30
+ * The plugin the dev server adds to the app's own.
31
+ *
32
+ * @param {string} base the dev server's base, `/_vite/`
33
+ * @returns {import('vite').Plugin}
34
+ */
35
+ export function styleXDevStyles(base) {
36
+ /** @type {null | (() => string)} */
37
+ let collect = null;
38
+ /** @type {string[]} the client build's inputs, as its config names them */
39
+ let inputs = [];
40
+ /** @type {string} */
41
+ let root = '';
42
+ /** @type {Promise<Set<string>> | null} the ids Vite resolves them to, once asked */
43
+ let entries = null;
44
+
45
+ return {
46
+ name: 'tinvar:stylex-dev',
47
+ apply: 'serve',
48
+
49
+ configResolved(config) {
50
+ // The app's StyleX plugin: the one that can say what it compiled.
51
+ const styleX = config.plugins.find(
52
+ (plugin) => typeof (/** @type {any} */ (plugin).__stylexCollectCss) === 'function',
53
+ );
54
+ collect = styleX ? () => /** @type {any} */ (styleX).__stylexCollectCss() ?? '' : null;
55
+ inputs = clientInputs(config);
56
+ root = config.root;
57
+ entries = null;
58
+ },
59
+
60
+ configureServer(server) {
61
+ if (collect === null) return;
62
+ const served = `${base}${STYLEX_DEV_CSS}`;
63
+ server.middlewares.use((req, res, next) => {
64
+ const url = (req.url ?? '').replace(/[?#].*$/, '');
65
+ if (url !== served) return next();
66
+ res.statusCode = 200;
67
+ res.setHeader('Content-Type', 'text/css; charset=utf-8');
68
+ res.setHeader('Cache-Control', 'no-store');
69
+ res.end(collect?.() ?? '');
70
+ });
71
+ },
72
+
73
+ resolveId(id) {
74
+ return collect !== null && id === STYLEX_DEV_MODULE ? RESOLVED : null;
75
+ },
76
+
77
+ load(id) {
78
+ if (id !== RESOLVED) return null;
79
+ return styleXDevRuntime(`${base}${STYLEX_DEV_CSS}`);
80
+ },
81
+
82
+ async transform(code, id) {
83
+ if (collect === null || this.environment?.name !== 'client') return null;
84
+ // Which modules the entries are is Vite's to say: an input may name
85
+ // one without its extension, or through an alias. Resolved once, as
86
+ // an import from the app's root would be.
87
+ const importer = path.join(root, 'index.html');
88
+ entries ??= Promise.all(inputs.map((input) => this.resolve(input, importer))).then(
89
+ (resolved) =>
90
+ new Set(resolved.filter((r) => r != null).map((r) => normalizePath(r.id.replace(/[?#].*$/, '')))),
91
+ );
92
+ if (!(await entries).has(normalizePath(id.replace(/[?#].*$/, '')))) return null;
93
+ // On the entry's first line: its source map's lines stay where they were.
94
+ return { code: `import ${JSON.stringify(STYLEX_DEV_MODULE)};${code}`, map: null };
95
+ },
96
+ };
97
+ }
98
+
99
+ /**
100
+ * The client environment's entries, as its build's rollup input names them.
101
+ *
102
+ * @param {import('vite').ResolvedConfig} config
103
+ * @returns {string[]}
104
+ */
105
+ function clientInputs(config) {
106
+ const input = /** @type {any} */ (config).environments?.client?.build?.rollupOptions?.input;
107
+ const named =
108
+ typeof input === 'string'
109
+ ? [input]
110
+ : Array.isArray(input)
111
+ ? input
112
+ : input && typeof input === 'object'
113
+ ? Object.values(input)
114
+ : [];
115
+ return named.filter((file) => typeof file === 'string');
116
+ }
117
+
118
+ /**
119
+ * The module the browser runs: the rules in a <style> of their own, fetched
120
+ * again after every update Vite applies, since an edit compiles new ones.
121
+ * Only the newest request's answer is shown: one edit sets off more than
122
+ * one fetch, and an older answer arriving last would put back rules from
123
+ * before it.
124
+ *
125
+ * @param {string} url
126
+ */
127
+ export function styleXDevRuntime(url) {
128
+ return `const URL = ${JSON.stringify(url)};
129
+ const ID = 'tinvar-stylex-dev';
130
+ let last = null;
131
+ let asked = 0;
132
+ function element() {
133
+ let el = document.getElementById(ID);
134
+ if (!el) {
135
+ el = document.createElement('style');
136
+ el.id = ID;
137
+ document.head.appendChild(el);
138
+ }
139
+ return el;
140
+ }
141
+ async function update() {
142
+ const request = ++asked;
143
+ try {
144
+ const response = await fetch(URL + '?t=' + Date.now(), { cache: 'no-store' });
145
+ if (!response.ok || request !== asked) return;
146
+ const css = await response.text();
147
+ if (request === asked && css !== last) {
148
+ element().textContent = css;
149
+ last = css;
150
+ }
151
+ } catch {}
152
+ }
153
+ update();
154
+ if (import.meta.hot) {
155
+ import.meta.hot.on('stylex:css-update', update);
156
+ import.meta.hot.on('vite:afterUpdate', () => setTimeout(update, 180));
157
+ }
158
+ `;
159
+ }