@tinvar/renderer 0.4.1 → 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 +1 -1
- package/src/dev-server.js +2 -0
- package/src/panel-styles.js +3 -2
- package/src/sandbox.js +72 -2
- package/src/stylex-dev.js +159 -0
package/package.json
CHANGED
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;
|
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/sandbox.js
CHANGED
|
@@ -166,6 +166,7 @@ export function sandboxFlags({ rendererRoot, appEntry, socketPath, extraRead = [
|
|
|
166
166
|
|
|
167
167
|
return [
|
|
168
168
|
permissionFlag(),
|
|
169
|
+
...networkFlags(),
|
|
169
170
|
...links.map((l) => `--allow-fs-read=${l}`),
|
|
170
171
|
// Without the ones a broader grant already covers: a package reached by
|
|
171
172
|
// its dependencies that is also in a node_modules granted for real.
|
|
@@ -189,6 +190,24 @@ export function permissionFlag() {
|
|
|
189
190
|
: '--experimental-permission';
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
/**
|
|
194
|
+
* The network, granted where Node's permission model governs it: from
|
|
195
|
+
* Node 25 it refuses every socket without `--allow-net`, the renderer's own
|
|
196
|
+
* among them, and the flag is all or nothing. Node 22's model has no
|
|
197
|
+
* network rule, so there the flag is unknown and left out. On both,
|
|
198
|
+
* lockNetwork() closes the network from inside the child -- every
|
|
199
|
+
* connection, lookup and datagram, and every listen but the renderer's
|
|
200
|
+
* socket -- so the renderer reaches the same as on Node 22 (ADR 0176).
|
|
201
|
+
*
|
|
202
|
+
* @returns {string[]}
|
|
203
|
+
*/
|
|
204
|
+
export function networkFlags() {
|
|
205
|
+
// Node calls --allow-net experimental, and says so once per renderer.
|
|
206
|
+
return process.allowedNodeEnvironmentFlags.has('--allow-net')
|
|
207
|
+
? ['--allow-net', '--disable-warning=ExperimentalWarning']
|
|
208
|
+
: [];
|
|
209
|
+
}
|
|
210
|
+
|
|
192
211
|
/**
|
|
193
212
|
* The app's build directory is granted whole, so it must be a build
|
|
194
213
|
* directory. Derived from the entry as its grandparent (dist/rsc/index.js
|
|
@@ -646,7 +665,9 @@ export const OPENS_NOTHING = Object.freeze({
|
|
|
646
665
|
/**
|
|
647
666
|
* Closes outbound networking from inside the sandboxed child.
|
|
648
667
|
*
|
|
649
|
-
* Node 22's permission model has no network flag,
|
|
668
|
+
* Node 22's permission model has no network flag, and from Node 25 the
|
|
669
|
+
* renderer is granted --allow-net to serve its socket (networkFlags()), so
|
|
670
|
+
* on either a dependency could
|
|
650
671
|
* open a socket to the database, or to anywhere, and send what it read. This
|
|
651
672
|
* replaces every function node:net, node:dgram and node:dns export but the
|
|
652
673
|
* few that open nothing (OPENS_NOTHING) -- TCP and Unix-socket connects
|
|
@@ -671,6 +692,15 @@ export const OPENS_NOTHING = Object.freeze({
|
|
|
671
692
|
* may listen on; without one it may listen on nothing
|
|
672
693
|
*/
|
|
673
694
|
export function lockNetwork({ net, dgram, dns, listenOn }) {
|
|
695
|
+
// Taken now, before any code the renderer loads runs: every original is
|
|
696
|
+
// called through these, never through a method looked up when it is
|
|
697
|
+
// called. A Function.prototype.call or apply swapped in later would be
|
|
698
|
+
// handed the original, and a WeakSet.prototype.has swapped in could
|
|
699
|
+
// answer yes for any handle (ADR 0176).
|
|
700
|
+
const apply = Reflect.apply;
|
|
701
|
+
const { has: weakHas, add: weakAdd } = WeakSet.prototype;
|
|
702
|
+
const isIn = (set, value) => apply(weakHas, set, [value]);
|
|
703
|
+
const putIn = (set, value) => apply(weakAdd, set, [value]);
|
|
674
704
|
const refuse = (what) =>
|
|
675
705
|
function refused() {
|
|
676
706
|
const error = new Error(
|
|
@@ -703,8 +733,48 @@ export function lockNetwork({ net, dgram, dns, listenOn }) {
|
|
|
703
733
|
// as the address for a socket file and null for a handle or descriptor.
|
|
704
734
|
const listen = net.Server.prototype._listen2;
|
|
705
735
|
const refuseListen = refuse('Listening anywhere but the renderer socket');
|
|
736
|
+
|
|
737
|
+
// The handle classes under net -- the Unix-socket one above all -- are
|
|
738
|
+
// reachable without a binding, as the constructor of a handle the
|
|
739
|
+
// process already holds: its standard input is a socket pair (the
|
|
740
|
+
// tether), and its own server's handle is one too. Their prototypes
|
|
741
|
+
// carry bind(), listen(), connect() and open(), which reach the network
|
|
742
|
+
// past everything above. So each one is held where it opens something:
|
|
743
|
+
// no connect and no open; a bind to the renderer's socket alone; and a
|
|
744
|
+
// listen only on the handle that bound it (ADR 0176).
|
|
745
|
+
const held = new WeakSet();
|
|
746
|
+
const bound = new WeakSet();
|
|
747
|
+
const hold = (Handle) => {
|
|
748
|
+
if (typeof Handle !== 'function' || isIn(held, Handle)) return;
|
|
749
|
+
putIn(held, Handle);
|
|
750
|
+
const proto = Handle.prototype;
|
|
751
|
+
for (const name of ['connect', 'connect6', 'open'])
|
|
752
|
+
if (typeof proto[name] === 'function') pin(proto, name, refuse(`A handle's ${name}()`));
|
|
753
|
+
for (const name of ['bind', 'bind6']) {
|
|
754
|
+
const original = proto[name];
|
|
755
|
+
if (typeof original !== 'function') continue;
|
|
756
|
+
pin(proto, name, function bindOwnSocket(address, ...rest) {
|
|
757
|
+
if (typeof listenOn !== 'string' || address !== listenOn) refuseListen();
|
|
758
|
+
const result = apply(original, this, [address, ...rest]);
|
|
759
|
+
putIn(bound, this);
|
|
760
|
+
return result;
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
const originalListen = proto.listen;
|
|
764
|
+
if (typeof originalListen === 'function')
|
|
765
|
+
pin(proto, 'listen', function listenOnBound(...args) {
|
|
766
|
+
if (!isIn(bound, this)) refuseListen();
|
|
767
|
+
return apply(originalListen, this, args);
|
|
768
|
+
});
|
|
769
|
+
};
|
|
770
|
+
// Made now, before their classes are held: Node makes each standard
|
|
771
|
+
// stream's handle on first use, with the open() held above.
|
|
772
|
+
for (const stream of [process.stdin, process.stdout, process.stderr]) hold(stream?._handle?.constructor);
|
|
773
|
+
|
|
706
774
|
pin(net.Server.prototype, '_listen2', function listenOnOwnSocket(address, port, ...rest) {
|
|
707
775
|
if (typeof listenOn !== 'string' || address !== listenOn || port !== -1) refuseListen();
|
|
708
|
-
|
|
776
|
+
const result = apply(listen, this, [address, port, ...rest]);
|
|
777
|
+
hold(this._handle?.constructor);
|
|
778
|
+
return result;
|
|
709
779
|
});
|
|
710
780
|
}
|
|
@@ -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
|
+
}
|