@tinvar/renderer 0.4.11 → 0.4.13

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.
@@ -42,7 +42,7 @@ const [socketPath, appRoot] = positional;
42
42
  const port = Number(option('port'));
43
43
  if (!socketPath || !appRoot || !Number.isInteger(port) || port < 1 || port > 65535) {
44
44
  console.error(
45
- 'usage: dev-renderer.js <socket-path> <app-root> --port=N [--deferred=FILE] [--codec=msgpack|json-lines] [--exit-with-stdin]',
45
+ 'usage: dev-renderer.js <socket-path> <app-root> --port=N [--deferred=FILE] [--public-host=NAME] [--codec=msgpack|json-lines] [--exit-with-stdin]',
46
46
  );
47
47
  process.exit(64);
48
48
  }
@@ -87,6 +87,8 @@ try {
87
87
  appRoot: path.resolve(appRoot),
88
88
  port,
89
89
  deferredManifest: option('deferred') ? path.resolve(option('deferred')) : null,
90
+ // A tunnel's name, which serve --dev's Caddyfile lets in (ADR 0190).
91
+ publicHost: option('public-host') ?? null,
90
92
  });
91
93
  } catch (error) {
92
94
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinvar/renderer",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
@@ -1,4 +1,5 @@
1
- // What the browser's dependency optimizer leaves alone in dev (ADR 0180).
1
+ // What the browser's dependency optimizer leaves alone in dev, and what it
2
+ // takes up in their place (ADR 0180, 0190).
2
3
  //
3
4
  // Vite bundles the packages the browser imports before it serves them.
4
5
  // @tinvar/react's client components are client references, which the RSC
@@ -8,18 +9,129 @@
8
9
  // call" until Vite reloaded it. The plugin says so, as "client component
9
10
  // dependency is inconsistently optimized". Served as its own modules, the
10
11
  // package loads once. What it imports, React alone, is optimized as before.
12
+ //
13
+ // A package StyleX leaves out -- every package it compiles from source,
14
+ // such as the skeleton's Meridian kit -- has its imports left out
15
+ // with it: Vite never looks inside it for what to bundle. So Meridian's
16
+ // lucide-react reached the browser as some 1,600 modules, one request each:
17
+ // slow on loopback, and through a tunnel more requests at once than the dev
18
+ // server's backlog or the tunnel would take. Its own dependencies are
19
+ // optimized instead, so each is one bundle, as the shop's vite.config has
20
+ // had lucide-react for a while. Each by its own name, where the app's root
21
+ // resolves it to the same package: then it is the one bundle the app's own
22
+ // imports of it reach too, and not a second copy beside it with its own
23
+ // React context. Through its parent (Vite's `parent > dependency`) only
24
+ // where the root reaches another copy, or none. A dependency the browser
25
+ // cannot take -- a native addon -- is left out by the app as any package
26
+ // is, in its client optimizeDeps.exclude, and is then not included here.
27
+
28
+ import { existsSync, readFileSync } from 'node:fs';
29
+ import path from 'node:path';
11
30
 
12
31
  /** Served to the browser as their own modules, never optimized into a bundle. */
13
32
  export const NOT_OPTIMIZED = ['@tinvar/react'];
14
33
 
15
34
  /** @returns {import('vite').Plugin} */
16
35
  export function clientDependencies() {
36
+ /** @type {string[]} */
37
+ let include = [];
38
+
17
39
  return {
18
40
  name: 'tinvar:client-dependencies',
19
41
  apply: 'serve',
42
+ // After StyleX, whose own config hook adds the packages it compiles.
43
+ enforce: 'post',
44
+ config(config) {
45
+ const root = path.resolve(config.root ?? process.cwd());
46
+ const excluded = new Set([
47
+ ...NOT_OPTIMIZED,
48
+ ...(config.optimizeDeps?.exclude ?? []),
49
+ ...(config.environments?.client?.optimizeDeps?.exclude ?? []),
50
+ ]);
51
+ const included = new Set([
52
+ ...(config.optimizeDeps?.include ?? []),
53
+ ...(config.environments?.client?.optimizeDeps?.include ?? []),
54
+ ]);
55
+ include = throughExcluded(root, excluded, included);
56
+ },
20
57
  configEnvironment(name) {
58
+ if (name !== 'client') return undefined;
21
59
  // Merged with the app's own: Vite concatenates the lists.
22
- return name === 'client' ? { optimizeDeps: { exclude: [...NOT_OPTIMIZED] } } : undefined;
60
+ return {
61
+ optimizeDeps: {
62
+ exclude: [...NOT_OPTIMIZED],
63
+ ...(include.length > 0 ? { include: [...include] } : {}),
64
+ },
65
+ };
23
66
  },
24
67
  };
25
68
  }
69
+
70
+ /**
71
+ * Whether a package is one StyleX compiles from source: one that depends on
72
+ * StyleX itself. Only such a package is a kit whose imports the browser
73
+ * needs. Another package left out -- the RSC plugin leaves itself out --
74
+ * is build tooling, whose dependencies are no browser's.
75
+ *
76
+ * @param {any} json
77
+ */
78
+ function compiledByStyleX(json) {
79
+ return ['dependencies', 'peerDependencies'].some((field) => {
80
+ const declared = json?.[field];
81
+ return declared && typeof declared === 'object' && Object.hasOwn(declared, '@stylexjs/stylex');
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Each installed dependency of each installed package StyleX left out: never a
87
+ * peer, which is the app's to provide and to include, never
88
+ * one left out itself, and never one the app includes already. By its own
89
+ * name where the root resolves it to the same package, else as
90
+ * `parent > dependency`.
91
+ *
92
+ * @param {string} root
93
+ * @param {Set<string>} excluded
94
+ * @param {Set<string>} included
95
+ * @returns {string[]}
96
+ */
97
+ function throughExcluded(root, excluded, included) {
98
+ const include = new Set();
99
+ for (const parent of excluded) {
100
+ if (NOT_OPTIMIZED.includes(parent)) continue;
101
+ const manifest = installed(root, parent);
102
+ if (manifest === null || !compiledByStyleX(manifest.json)) continue;
103
+ const declared = manifest.json?.dependencies;
104
+ const dependencies =
105
+ declared && typeof declared === 'object' && !Array.isArray(declared) ? Object.keys(declared) : [];
106
+ for (const dependency of dependencies) {
107
+ if (excluded.has(dependency) || included.has(dependency)) continue;
108
+ const own = installed(manifest.dir, dependency);
109
+ if (own === null) continue;
110
+ include.add(installed(root, dependency)?.dir === own.dir ? dependency : `${parent} > ${dependency}`);
111
+ }
112
+ }
113
+ return [...include];
114
+ }
115
+
116
+ /**
117
+ * A package's manifest as Node would find it from `from`: in the nearest
118
+ * node_modules that holds it, walking up. Read from disk, since a package's
119
+ * exports need not offer its package.json.
120
+ *
121
+ * @param {string} from
122
+ * @param {string} name
123
+ * @returns {{ dir: string, json: any } | null}
124
+ */
125
+ function installed(from, name) {
126
+ for (let dir = from; ; dir = path.dirname(dir)) {
127
+ const file = path.join(dir, 'node_modules', name, 'package.json');
128
+ if (existsSync(file)) {
129
+ try {
130
+ return { dir: path.dirname(file), json: JSON.parse(readFileSync(file, 'utf8')) };
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ if (path.dirname(dir) === dir) return null;
136
+ }
137
+ }
package/src/dev-server.js CHANGED
@@ -55,9 +55,11 @@ function configFileOf(appRoot) {
55
55
  * Starts Vite's dev server on loopback for the app at `appRoot`, and points
56
56
  * the renderer's dev entry at its rsc environment.
57
57
  *
58
- * @param {{ appRoot: string, port: number, host?: string, deferredManifest?: string | null, logLevel?: string }} options
58
+ * @param {{ appRoot: string, port: number, host?: string, deferredManifest?: string | null, publicHost?: string | null, logLevel?: string }} options
59
59
  * `deferredManifest` is where to keep the views' <Deferred> components
60
- * up to date, as the build's deferredBoundaries() writes them
60
+ * up to date, as the build's deferredBoundaries() writes them;
61
+ * `publicHost` the one name a tunnel serves the app under, which
62
+ * Vite's host check lets in beside loopback's (ADR 0190)
61
63
  * @returns {Promise<{ vite: any, url: string, close: () => Promise<void> }>}
62
64
  */
63
65
  export async function createDevServer({
@@ -65,6 +67,7 @@ export async function createDevServer({
65
67
  port,
66
68
  host = '127.0.0.1',
67
69
  deferredManifest = null,
70
+ publicHost = null,
68
71
  logLevel,
69
72
  }) {
70
73
  assertDevelopment();
@@ -120,6 +123,9 @@ export async function createDevServer({
120
123
  host,
121
124
  port,
122
125
  strictPort: true,
126
+ // Vite answers loopback's names alone, against DNS rebinding, as the
127
+ // Caddyfile in front of it does; a tunnel's one name besides.
128
+ ...(publicHost ? { allowedHosts: [publicHost] } : {}),
123
129
  fs: {
124
130
  // Vite's own refusals, and the secrets an app keeps beside its code:
125
131
  // a key file, a database, the PHP dependencies.