@tinvar/renderer 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/bin/dev-renderer.js +117 -0
- package/bin/renderer-probe.js +52 -0
- package/bin/renderer.js +327 -0
- package/package.json +44 -4
- package/src/asset-origin.js +306 -0
- package/src/build-profile.js +108 -0
- package/src/can-branch.js +1666 -0
- package/src/classify.js +157 -0
- package/src/client-chunks.js +54 -0
- package/src/client-props.js +540 -0
- package/src/codec.js +297 -0
- package/src/dev-app.js +45 -0
- package/src/dev-guard.js +33 -0
- package/src/dev-server.js +460 -0
- package/src/eslint.js +469 -0
- package/src/head.js +524 -0
- package/src/html-stream.js +479 -0
- package/src/log.js +131 -0
- package/src/mint-client.js +142 -0
- package/src/panel-styles.js +254 -0
- package/src/pool.js +664 -0
- package/src/protocol.js +71 -0
- package/src/query-client.js +198 -0
- package/src/query-map.js +116 -0
- package/src/render-session.js +580 -0
- package/src/sandbox.js +710 -0
- package/src/server.js +466 -0
- package/src/socket-group.js +72 -0
- package/src/vite.js +2416 -0
- package/src/vitest.d.ts +20 -0
- package/src/vitest.js +179 -0
- package/src/warm.d.ts +9 -0
- package/src/warm.js +60 -0
- package/src/workbench.d.ts +146 -0
- package/src/workbench.js +754 -0
- package/README.md +0 -3
package/src/classify.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// What kind of failure a render threw, read from the error itself.
|
|
2
|
+
//
|
|
3
|
+
// tinvar-spec.md, "Operations and tooling": "Classify errors (browser API
|
|
4
|
+
// used on the server, component not found, render error) with hints." The
|
|
5
|
+
// classification is made here, where the error object is, and travels to
|
|
6
|
+
// PHP as a code on the error frame. The hint does not: PHP keeps the text
|
|
7
|
+
// for each code itself, so what a log line or the dev overlay advises is
|
|
8
|
+
// the framework's own words, never something the renderer -- the side that
|
|
9
|
+
// runs third-party npm code -- chose to send.
|
|
10
|
+
//
|
|
11
|
+
// Matched on the messages React, Node and Vite write. A message nobody
|
|
12
|
+
// recognises is a plain render error, which is what it was called before
|
|
13
|
+
// any of this existed.
|
|
14
|
+
|
|
15
|
+
/** The codes, mirrored by Tinvar\Render\RenderFailure on PHP's side. */
|
|
16
|
+
export const FAILURE = {
|
|
17
|
+
BROWSER_API: 'browser_api_on_server',
|
|
18
|
+
COMPONENT_NOT_FOUND: 'component_not_found',
|
|
19
|
+
NOT_SERIALIZABLE: 'not_serializable',
|
|
20
|
+
CLIENT_API_ON_SERVER: 'client_only_api_on_server',
|
|
21
|
+
ASYNC_CLIENT_COMPONENT: 'async_client_component',
|
|
22
|
+
QUERY_FAILED: 'query_failed',
|
|
23
|
+
RENDER_ERROR: 'render_failed',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Globals only a browser has. Read as a ReferenceError on Node, which has none of them. */
|
|
27
|
+
const BROWSER_GLOBALS = new Set([
|
|
28
|
+
'window',
|
|
29
|
+
'document',
|
|
30
|
+
'navigator',
|
|
31
|
+
'location',
|
|
32
|
+
'history',
|
|
33
|
+
'localStorage',
|
|
34
|
+
'sessionStorage',
|
|
35
|
+
'indexedDB',
|
|
36
|
+
'alert',
|
|
37
|
+
'confirm',
|
|
38
|
+
'prompt',
|
|
39
|
+
'matchMedia',
|
|
40
|
+
'getComputedStyle',
|
|
41
|
+
'requestAnimationFrame',
|
|
42
|
+
'cancelAnimationFrame',
|
|
43
|
+
'requestIdleCallback',
|
|
44
|
+
'IntersectionObserver',
|
|
45
|
+
'ResizeObserver',
|
|
46
|
+
'MutationObserver',
|
|
47
|
+
'HTMLElement',
|
|
48
|
+
'Element',
|
|
49
|
+
'Node',
|
|
50
|
+
'Image',
|
|
51
|
+
'FileReader',
|
|
52
|
+
'XMLHttpRequest',
|
|
53
|
+
'self',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** Reading a property of a browser global that a module stubbed out, as `window?.x` guards do not. */
|
|
57
|
+
const BROWSER_PROPERTY =
|
|
58
|
+
/^Cannot read propert(?:y|ies) of undefined \(reading '(?:addEventListener|removeEventListener|querySelector|getElementById|localStorage|sessionStorage)'\)/;
|
|
59
|
+
|
|
60
|
+
const COMPONENT_NOT_FOUND = [
|
|
61
|
+
/^Unknown route '/,
|
|
62
|
+
/^Unknown deferred component '/,
|
|
63
|
+
/^Element type is invalid/,
|
|
64
|
+
/Could not find the module .* in the React (?:Client|Server) Manifest/,
|
|
65
|
+
/^Failed to load url /,
|
|
66
|
+
/^Cannot find module /,
|
|
67
|
+
/does not provide an export named/,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const NOT_SERIALIZABLE = [
|
|
71
|
+
/Functions cannot be passed directly to Client Components/,
|
|
72
|
+
/Event handlers cannot be passed to Client Component props/,
|
|
73
|
+
/Only plain objects(?:,| and) .* can be passed to Client Components/,
|
|
74
|
+
/Classes or other objects with methods are not supported/,
|
|
75
|
+
/Objects with toJSON methods are not supported/,
|
|
76
|
+
/Symbols cannot be passed to Client Components/,
|
|
77
|
+
/BigInt .* serialize/i,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/** A hook or a client-only React API called by a server component, under the react-server build. */
|
|
81
|
+
const CLIENT_API = [
|
|
82
|
+
/\b(?:use(?:State|Effect|LayoutEffect|InsertionEffect|Reducer|Ref|Context|ImperativeHandle|SyncExternalStore|Transition|DeferredValue|Optimistic|ActionState|FormStatus)|createContext|Component|PureComponent)\b.* is not a function/,
|
|
83
|
+
/only works in Client Components/,
|
|
84
|
+
/is not supported in React Server Components/,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const ASYNC_CLIENT = [
|
|
88
|
+
/async\/await is not yet supported in Client Components/,
|
|
89
|
+
/is an async Client Component/,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {unknown} error
|
|
94
|
+
* @returns {string} one of FAILURE
|
|
95
|
+
*/
|
|
96
|
+
export function classify(error) {
|
|
97
|
+
if (error && typeof error === 'object' && error.name === 'QueryFailed') return FAILURE.QUERY_FAILED;
|
|
98
|
+
|
|
99
|
+
const message = String(error?.message ?? error ?? '');
|
|
100
|
+
|
|
101
|
+
if (error?.name === 'ReferenceError') {
|
|
102
|
+
const name = /^(\S+) is not defined$/.exec(message)?.[1];
|
|
103
|
+
if (name !== undefined && BROWSER_GLOBALS.has(name)) return FAILURE.BROWSER_API;
|
|
104
|
+
}
|
|
105
|
+
if (BROWSER_PROPERTY.test(message)) return FAILURE.BROWSER_API;
|
|
106
|
+
if (ASYNC_CLIENT.some((pattern) => pattern.test(message))) return FAILURE.ASYNC_CLIENT_COMPONENT;
|
|
107
|
+
if (NOT_SERIALIZABLE.some((pattern) => pattern.test(message))) return FAILURE.NOT_SERIALIZABLE;
|
|
108
|
+
if (CLIENT_API.some((pattern) => pattern.test(message))) return FAILURE.CLIENT_API_ON_SERVER;
|
|
109
|
+
if (COMPONENT_NOT_FOUND.some((pattern) => pattern.test(message))) return FAILURE.COMPONENT_NOT_FOUND;
|
|
110
|
+
|
|
111
|
+
return FAILURE.RENDER_ERROR;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The first frame of a stack that is the application's own code: not
|
|
116
|
+
* React, not Vite, not Node -- the line a person would open.
|
|
117
|
+
*
|
|
118
|
+
* In dev the module runner maps stacks back to the source, so this is a
|
|
119
|
+
* line of a .tsx file; from a build it is a line of the bundle, which is
|
|
120
|
+
* still where to start.
|
|
121
|
+
*
|
|
122
|
+
* @param {unknown} error
|
|
123
|
+
* @returns {{ file: string, line: number, column: number } | null}
|
|
124
|
+
*/
|
|
125
|
+
export function locate(error) {
|
|
126
|
+
const stack = typeof error?.stack === 'string' ? error.stack : '';
|
|
127
|
+
for (const line of stack.split('\n').slice(1)) {
|
|
128
|
+
const match = /\(?((?:file:\/\/)?\/[^():]+):(\d+):(\d+)\)?\s*$/.exec(line.trim());
|
|
129
|
+
if (!match) continue;
|
|
130
|
+
const file = match[1].replace(/^file:\/\//, '');
|
|
131
|
+
if (/\/node_modules\/|^node:|\/packages\/renderer\//.test(file)) continue;
|
|
132
|
+
return { file, line: Number(match[2]), column: Number(match[3]) };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The longest stack sent to PHP. A stack is for a person to read, not to fill a frame. */
|
|
138
|
+
export const MAX_STACK_BYTES = 16384;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The fields an error frame carries about its cause: the code, and where it
|
|
142
|
+
* happened. PHP shows them in the dev overlay and drops them otherwise.
|
|
143
|
+
*
|
|
144
|
+
* @param {unknown} error
|
|
145
|
+
* @returns {{ code: string, stack?: string, file?: string, line?: number }}
|
|
146
|
+
*/
|
|
147
|
+
export function describe(error) {
|
|
148
|
+
const out = { code: classify(error) };
|
|
149
|
+
if (typeof error?.stack === 'string' && error.stack !== '')
|
|
150
|
+
out.stack = error.stack.slice(0, MAX_STACK_BYTES);
|
|
151
|
+
const at = locate(error);
|
|
152
|
+
if (at) {
|
|
153
|
+
out.file = at.file.slice(0, 1024);
|
|
154
|
+
out.line = at.line;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// One chunk per client component, for @vitejs/plugin-rsc's `clientChunks`.
|
|
2
|
+
//
|
|
3
|
+
// tinvar-spec.md, "Operations and tooling": "Code splitting: one chunk per
|
|
4
|
+
// client component; `modulepreload` for the current page's client
|
|
5
|
+
// references, capped so `Link` headers never overflow server buffers."
|
|
6
|
+
//
|
|
7
|
+
// The plugin groups client references by the server chunk that refers to
|
|
8
|
+
// them, and an app's rsc build is one server chunk -- so every 'use client'
|
|
9
|
+
// module the app has went into one client chunk, and a page with one button
|
|
10
|
+
// on it downloaded every page's islands. Named here by module, each
|
|
11
|
+
// reference is a chunk of its own: a page's payload names the references it
|
|
12
|
+
// renders, the browser loads those, and the head preloads those alone.
|
|
13
|
+
// What several of them import -- React, a kit's internals, the router -- the
|
|
14
|
+
// bundler still puts in shared chunks, so each module is one instance
|
|
15
|
+
// however many chunks reach it.
|
|
16
|
+
//
|
|
17
|
+
// A dependency's modules are named the same way, one chunk each, not one per
|
|
18
|
+
// package: a kit's client components are as separate as the app's, and a
|
|
19
|
+
// page using its toast has no need of its tabs. Only the modules the server
|
|
20
|
+
// graph renders are client references at all -- the shop's kit is six
|
|
21
|
+
// modules and its icons one, not a module per icon -- and ModulePreload caps
|
|
22
|
+
// the header however many a page reaches (ADR 0036).
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The chunk a client reference goes in: a name of its own, from its path
|
|
26
|
+
* relative to the Vite root -- after the last `node_modules/` for a
|
|
27
|
+
* dependency, without leading `../` for a workspace package -- with no
|
|
28
|
+
* query and no extension. Distinct modules get distinct names, so no two
|
|
29
|
+
* share a chunk; the bundler names the file from the last segment.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} normalizedId the reference's id as the plugin gives it, relative to the root
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function clientChunkName(normalizedId) {
|
|
35
|
+
let name = normalizedId.replaceAll('\\', '/').replace(/[?#].*$/, '');
|
|
36
|
+
const modules = name.lastIndexOf('node_modules/');
|
|
37
|
+
name = modules === -1 ? name.replace(/^(?:\.\.?\/)+/, '') : name.slice(modules + 'node_modules/'.length);
|
|
38
|
+
name = name.replace(/\.[cm]?[jt]sx?$/, '');
|
|
39
|
+
// Always a path of two parts at least. panelChunks() names a panel's
|
|
40
|
+
// chunk `panel-<name>`, one part, and the plugin puts every reference of
|
|
41
|
+
// one name in one chunk: a component at the root named panel-admin.tsx
|
|
42
|
+
// went into the admin panel's, and a customer page rendering it loaded
|
|
43
|
+
// all the admin's code.
|
|
44
|
+
return name.includes('/') ? name : `root/${name}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The `clientChunks` option: `rsc({ clientChunks: clientComponentChunks() })`.
|
|
49
|
+
*
|
|
50
|
+
* @returns {(meta: { id: string, normalizedId: string, serverChunk: string }) => string}
|
|
51
|
+
*/
|
|
52
|
+
export function clientComponentChunks() {
|
|
53
|
+
return (meta) => clientChunkName(meta.normalizedId);
|
|
54
|
+
}
|