gesso-vite-plugin 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +584 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# gesso-vite-plugin
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
`gesso()` finds an application's worker entries, writes the constructions and
|
|
8
|
+
the hot-replacement wiring, and mounts the development error overlay.
|
|
9
|
+
|
|
10
|
+
It is optional by design: nothing in `gesso-core` or `gesso-framework`
|
|
11
|
+
mentions Vite, and the literal `new Worker(new URL(...))` construction stays
|
|
12
|
+
the documented fallback. The plugin's factories go in first, so your own
|
|
13
|
+
options spread over them.
|
|
14
|
+
|
|
15
|
+
It also names the one failure that looks like a bug in the framework: a save
|
|
16
|
+
that reloads the page instead of replacing a module, because a module is
|
|
17
|
+
reached from the main thread as well as from the render worker.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kevin Baker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# gesso-vite-plugin
|
|
2
|
+
|
|
3
|
+
Finds a Gesso application's worker entries, writes the constructions and the hot-replacement wiring, and says when a save will reload the page.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install --save-dev gesso-vite-plugin
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// vite.config.ts
|
|
11
|
+
import { gesso } from 'gesso-vite-plugin';
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [gesso()]
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then the worker boilerplate goes away:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// main.ts
|
|
22
|
+
createApp({ history: { mode: 'path' } }).mount('#app');
|
|
23
|
+
|
|
24
|
+
// RenderWorker.ts
|
|
25
|
+
renderRoot(AppRoot).useService(Feed);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## It is optional, deliberately
|
|
29
|
+
|
|
30
|
+
Nothing in `gesso-core` or `gesso-framework` mentions Vite, and no bundler is imported into the framework. Everything this plugin writes, you can write by hand: the literal `new Worker(new URL(...))` construction stays the documented fallback, and the plugin's factories go in first so your own options spread over them.
|
|
31
|
+
|
|
32
|
+
## The diagnostic that is worth the install
|
|
33
|
+
|
|
34
|
+
One failure looks exactly like a bug in the framework: a save that reloads the whole page instead of replacing a module. The cause is a module reached from the main thread as well as from the render worker, which Vite cannot hot-replace because it propagates the invalidation to a main-thread importer that does not accept it. The plugin says so, and names the module, instead of leaving you to guess.
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
[The Vite plugin](https://github.com/kevinpbaker/gesso/blob/main/apps/docs/tooling/vite-plugin.md)
|
|
39
|
+
|
|
40
|
+
MIT (c) Kevin Baker
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
//#region src/render.d.ts
|
|
3
|
+
/** What the plugin found in a render worker entry, and could wire. */
|
|
4
|
+
interface RenderWiring {
|
|
5
|
+
/** The rewritten module, or null when there was nothing to wire. */
|
|
6
|
+
readonly code: string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Why nothing was wired, when nothing was. A sentence for the
|
|
9
|
+
* developer, not an error: an entry the plugin cannot wire still
|
|
10
|
+
* runs, it just full-reloads on a save.
|
|
11
|
+
*/
|
|
12
|
+
readonly skipped: string | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Writes the two lines an author would otherwise write by hand.
|
|
16
|
+
*
|
|
17
|
+
* The example below is the specification:
|
|
18
|
+
*
|
|
19
|
+
* const app = renderRoot(LiveApp).useService(LiveFeed);
|
|
20
|
+
* import.meta.hot?.accept('./LiveExampleApp', module => {
|
|
21
|
+
* app.reload(module.LiveApp, [module.LiveFeed]);
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* Three things have to be found for that to be written mechanically:
|
|
25
|
+
* the handle the chain returns, the module the root component came
|
|
26
|
+
* from, and the modules the services came from. The handle is taken by
|
|
27
|
+
* routing the call through a wrapper of the plugin's own, which keeps
|
|
28
|
+
* the chain that follows it intact whether or not the author assigned
|
|
29
|
+
* it. The modules come from the entry's own import statements, which is
|
|
30
|
+
* exactly the question `importSources` answers.
|
|
31
|
+
*
|
|
32
|
+
* A service declared in a module of its own is accepted alongside the
|
|
33
|
+
* root, because a registry keyed by class object cannot tell a
|
|
34
|
+
* replacement from a stranger and `reload` is where that is repaired.
|
|
35
|
+
* What is deliberately *not* wired is `useRoutes`: routes are read once
|
|
36
|
+
* when the runtime is built, so a change to the route table is a full
|
|
37
|
+
* reload and saying so is better than replacing a tree that still
|
|
38
|
+
* resolves the old routes.
|
|
39
|
+
*/
|
|
40
|
+
declare function transformRenderWorker(code: string): RenderWiring;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/source.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* Just enough of a JavaScript scanner to find a call and to say which
|
|
45
|
+
* module a name was imported from.
|
|
46
|
+
*
|
|
47
|
+
* Written here rather than taken from npm for the reason the rest of
|
|
48
|
+
* this repository's tooling gives (`packages/devtools/src/sourceMap.ts`,
|
|
49
|
+
* `scripts/gen-layout-fixtures.ts`): the whole of what the plugin needs
|
|
50
|
+
* is "where does `createApp(` open and close" and "where did `SegueApp`
|
|
51
|
+
* come from", and a parser is a dependency, a TypeScript dialect
|
|
52
|
+
* question and a version to keep in step, in exchange for two
|
|
53
|
+
* questions a scanner answers.
|
|
54
|
+
*
|
|
55
|
+
* The trick that makes it safe is `blankLiterals`: every string,
|
|
56
|
+
* template and comment is replaced by spaces of the same length, so the
|
|
57
|
+
* positions found in the blanked copy are positions in the original,
|
|
58
|
+
* and no brace inside a string can be mistaken for code. Everything
|
|
59
|
+
* below searches the blank copy and slices the original.
|
|
60
|
+
*
|
|
61
|
+
* What it does not do is understand scope. A `createApp` that a module
|
|
62
|
+
* defined for itself would be found the same way an imported one is,
|
|
63
|
+
* which is why the plugin checks the imports first and only then looks
|
|
64
|
+
* for the call.
|
|
65
|
+
*/
|
|
66
|
+
/** Where a call expression starts, where its arguments are, where it ends. */
|
|
67
|
+
interface CallSite {
|
|
68
|
+
/** Index of the first character of the callee name. */
|
|
69
|
+
readonly start: number;
|
|
70
|
+
/** Index just after the opening parenthesis. */
|
|
71
|
+
readonly argsStart: number;
|
|
72
|
+
/** Index of the closing parenthesis. */
|
|
73
|
+
readonly argsEnd: number;
|
|
74
|
+
/** Index just after the closing parenthesis. */
|
|
75
|
+
readonly end: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A copy of `code` in which every string, template literal, regular
|
|
79
|
+
* expression body and comment is spaces, and everything else is
|
|
80
|
+
* untouched.
|
|
81
|
+
*
|
|
82
|
+
* Same length as the input, and newlines are kept, so an index into
|
|
83
|
+
* the result is an index into the original and a line number is still
|
|
84
|
+
* a line number. String and template delimiters are kept as well, so a
|
|
85
|
+
* specifier can be found by its quotes here and read from there.
|
|
86
|
+
*/
|
|
87
|
+
declare function blankLiterals(code: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Every call of `name` in the module, in source order.
|
|
90
|
+
*
|
|
91
|
+
* A bare name has to stand alone: `renderRoot(` matches, `myRenderRoot(`
|
|
92
|
+
* and `thing.renderRoot(` do not, because a method of that name on some
|
|
93
|
+
* other object is not the framework's function. A name written with a
|
|
94
|
+
* leading dot is the opposite and matches only the method call:
|
|
95
|
+
* `.useService(` is how a builder chain reads and the only way it is
|
|
96
|
+
* ever written.
|
|
97
|
+
*/
|
|
98
|
+
declare function findCalls(code: string, name: string, blank?: string): CallSite[];
|
|
99
|
+
/** The first call of `name`, or null. */
|
|
100
|
+
declare function findCall(code: string, name: string, blank?: string): CallSite | null;
|
|
101
|
+
/**
|
|
102
|
+
* The identifier a call's first argument is, or null when the argument
|
|
103
|
+
* is anything more complicated than a name.
|
|
104
|
+
*
|
|
105
|
+
* `renderRoot(SegueApp)` is the shape every entry in this repository
|
|
106
|
+
* writes and the only one that can be hot-replaced, because a
|
|
107
|
+
* replacement has to be looked up by name in the module that changed.
|
|
108
|
+
* `renderRoot(<App x={1} />)` is legal and gets no HMR wiring, which
|
|
109
|
+
* the plugin says rather than guessing.
|
|
110
|
+
*/
|
|
111
|
+
declare function firstArgumentName(code: string, call: CallSite, blank?: string): string | null;
|
|
112
|
+
/**
|
|
113
|
+
* Which module each imported name came from: local name to specifier,
|
|
114
|
+
* exactly as the import wrote it.
|
|
115
|
+
*
|
|
116
|
+
* Default and namespace imports are recorded under their local names
|
|
117
|
+
* too, so `import App from './App'` and `import * as app from './App'`
|
|
118
|
+
* both answer. What the map does not carry is whether the binding is a
|
|
119
|
+
* type, because `import type` is stripped before this runs in every
|
|
120
|
+
* configuration that matters and a type never reaches a call.
|
|
121
|
+
*/
|
|
122
|
+
declare function importSources(code: string, blank?: string): Map<string, string>;
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/shell.d.ts
|
|
125
|
+
/** Where the plugin found the two worker entries, as the shell imports them. */
|
|
126
|
+
interface WorkerEntries {
|
|
127
|
+
/** A specifier relative to the shell module, e.g. `./RenderWorker.ts`. */
|
|
128
|
+
readonly renderWorker: string;
|
|
129
|
+
/** The same for the application worker, or null when there is none. */
|
|
130
|
+
readonly appLogicWorker: string | null;
|
|
131
|
+
}
|
|
132
|
+
/** A `createApp` call the plugin could write for, and what it still needs. */
|
|
133
|
+
interface ShellCall {
|
|
134
|
+
readonly call: CallSite;
|
|
135
|
+
/**
|
|
136
|
+
* False when the author wrote `renderWorker` themselves, which is the
|
|
137
|
+
* documented fallback and the case where the plugin has no worker to
|
|
138
|
+
* find. It still wires the overlay in.
|
|
139
|
+
*/
|
|
140
|
+
readonly needsWorkers: boolean;
|
|
141
|
+
}
|
|
142
|
+
interface ShellTransformOptions {
|
|
143
|
+
/** The entries to construct, or null when the author named them. */
|
|
144
|
+
readonly entries: WorkerEntries | null;
|
|
145
|
+
/**
|
|
146
|
+
* Whether to wire the error overlay in as well. True while the dev
|
|
147
|
+
* server is running and never in a build, so a production bundle
|
|
148
|
+
* carries no reference to `gesso-devtools`.
|
|
149
|
+
*/
|
|
150
|
+
readonly overlay: boolean;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The `createApp` call in a module, when the module has one that came
|
|
154
|
+
* from the framework.
|
|
155
|
+
*
|
|
156
|
+
* Separate from the transform because finding the worker entries is an
|
|
157
|
+
* asynchronous resolution against the bundler and worth doing only for
|
|
158
|
+
* a module that turns out to be a shell, which is one module in an
|
|
159
|
+
* application.
|
|
160
|
+
*/
|
|
161
|
+
declare function findShellCall(code: string, blank?: string): ShellCall | null;
|
|
162
|
+
/**
|
|
163
|
+
* Rewrites the shell module so that it names no worker.
|
|
164
|
+
*
|
|
165
|
+
* The one edit inside the author's own code is the call itself:
|
|
166
|
+
* `createApp(...)` becomes `createApp(__gessoOptions(...))`, on the
|
|
167
|
+
* same line, so every line number in the module is the one it was.
|
|
168
|
+
* Everything else is appended below, as function declarations, which
|
|
169
|
+
* hoist and are therefore usable from the line above them.
|
|
170
|
+
*
|
|
171
|
+
* The merge order is what keeps the promise that the
|
|
172
|
+
* literal construction stays the documented fallback: the plugin's
|
|
173
|
+
* factories go in first and the author's options spread over them, so
|
|
174
|
+
* a `renderWorker` written by hand still wins.
|
|
175
|
+
*
|
|
176
|
+
* Returns null when there is nothing to do, which is the common case:
|
|
177
|
+
* most modules do not call `createApp`.
|
|
178
|
+
*/
|
|
179
|
+
declare function transformShell(code: string, options: ShellTransformOptions): string | null;
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/index.d.ts
|
|
182
|
+
/**
|
|
183
|
+
* `gesso-vite-plugin` — the plumbing between saving a file and seeing
|
|
184
|
+
* the result.
|
|
185
|
+
*
|
|
186
|
+
* A Gesso application is three files either side of a thread barrier
|
|
187
|
+
*, and until now the barrier cost the author four
|
|
188
|
+
* incantations: `new Worker(new URL('./RenderWorker.ts',
|
|
189
|
+
* import.meta.url), { type: 'module' })` twice, because only a literal
|
|
190
|
+
* gets a chunk; `import.meta.hot.accept` with the root module named by
|
|
191
|
+
* hand; and `mountErrorOverlay` wired to `onError`, without which a
|
|
192
|
+
* worker exception reaches a console nobody has selected. None of them
|
|
193
|
+
* is a decision. All of them are written the same way in every
|
|
194
|
+
* application, and getting one wrong fails silently.
|
|
195
|
+
*
|
|
196
|
+
* The three-file shape stays. What this removes is the incantations,
|
|
197
|
+
* and what it adds is the one diagnostic that cannot be read off the
|
|
198
|
+
* screen: a save that reloads the page rather than replacing a module,
|
|
199
|
+
* which traces to the root module having a
|
|
200
|
+
* main-thread importer and which otherwise looks like HMR simply not
|
|
201
|
+
* working.
|
|
202
|
+
*
|
|
203
|
+
* // vite.config.ts
|
|
204
|
+
* import { gesso } from 'gesso-vite-plugin';
|
|
205
|
+
* export default defineConfig({ plugins: [gesso()] });
|
|
206
|
+
*
|
|
207
|
+
* // main.ts
|
|
208
|
+
* createApp({ history: { mode: 'path' } }).mount('#app');
|
|
209
|
+
*
|
|
210
|
+
* The literal construction stays supported and stays documented: the
|
|
211
|
+
* plugin merges its factories *under* the options the author wrote, so
|
|
212
|
+
* a hand-written `renderWorker` wins and an application that would
|
|
213
|
+
* rather say it out loud can.
|
|
214
|
+
*/
|
|
215
|
+
interface GessoPluginOptions {
|
|
216
|
+
/**
|
|
217
|
+
* The render worker entry, as a specifier the shell module could
|
|
218
|
+
* import. By default the plugin tries the names below, in order, and
|
|
219
|
+
* uses the first that resolves beside the shell.
|
|
220
|
+
*/
|
|
221
|
+
readonly renderWorker?: string;
|
|
222
|
+
/**
|
|
223
|
+
* The application worker entry, or false for an application that has
|
|
224
|
+
* none. By default the plugin looks for it and leaves it out when
|
|
225
|
+
* there is nothing to find, which is the single-worker shape the
|
|
226
|
+
* scaffold produces.
|
|
227
|
+
*/
|
|
228
|
+
readonly appLogicWorker?: string | false;
|
|
229
|
+
/**
|
|
230
|
+
* Wire `gesso-devtools`'s error overlay into `onError` (default
|
|
231
|
+
* true). Only ever in a dev server: a build carries no reference to
|
|
232
|
+
* the package.
|
|
233
|
+
*/
|
|
234
|
+
readonly overlay?: boolean;
|
|
235
|
+
/** Emit the `import.meta.hot.accept` wiring (default true). */
|
|
236
|
+
readonly hmr?: boolean;
|
|
237
|
+
/**
|
|
238
|
+
* Report a save that will reload the page instead of replacing a
|
|
239
|
+
* module (default true).
|
|
240
|
+
*/
|
|
241
|
+
readonly diagnostics?: boolean;
|
|
242
|
+
}
|
|
243
|
+
declare function gesso(options?: GessoPluginOptions): Plugin;
|
|
244
|
+
//#endregion
|
|
245
|
+
export { type CallSite, GessoPluginOptions, type RenderWiring, type ShellCall, type ShellTransformOptions, type WorkerEntries, blankLiterals, gesso as default, gesso, findCall, findCalls, findShellCall, firstArgumentName, importSources, transformRenderWorker, transformShell };
|
|
246
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
//#region src/source.ts
|
|
2
|
+
const IDENTIFIER = /[A-Za-z0-9_$]/;
|
|
3
|
+
/**
|
|
4
|
+
* A copy of `code` in which every string, template literal, regular
|
|
5
|
+
* expression body and comment is spaces, and everything else is
|
|
6
|
+
* untouched.
|
|
7
|
+
*
|
|
8
|
+
* Same length as the input, and newlines are kept, so an index into
|
|
9
|
+
* the result is an index into the original and a line number is still
|
|
10
|
+
* a line number. String and template delimiters are kept as well, so a
|
|
11
|
+
* specifier can be found by its quotes here and read from there.
|
|
12
|
+
*/
|
|
13
|
+
function blankLiterals(code) {
|
|
14
|
+
const out = code.split("");
|
|
15
|
+
let index = 0;
|
|
16
|
+
let regexAllowed = true;
|
|
17
|
+
const blank = (from, to) => {
|
|
18
|
+
for (let i = from; i < to; i++) if (out[i] !== "\n") out[i] = " ";
|
|
19
|
+
};
|
|
20
|
+
while (index < code.length) {
|
|
21
|
+
const char = code[index];
|
|
22
|
+
if (char === "/" && code[index + 1] === "/") {
|
|
23
|
+
const end = code.indexOf("\n", index);
|
|
24
|
+
blank(index, end === -1 ? code.length : end);
|
|
25
|
+
index = end === -1 ? code.length : end;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (char === "/" && code[index + 1] === "*") {
|
|
29
|
+
const end = code.indexOf("*/", index + 2);
|
|
30
|
+
const stop = end === -1 ? code.length : end + 2;
|
|
31
|
+
blank(index, stop);
|
|
32
|
+
index = stop;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (char === "\"" || char === "'") {
|
|
36
|
+
const end = closingQuote(code, index, char);
|
|
37
|
+
blank(index + 1, end);
|
|
38
|
+
index = end + 1;
|
|
39
|
+
regexAllowed = false;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === "`") {
|
|
43
|
+
const end = closingQuote(code, index, "`");
|
|
44
|
+
blank(index + 1, end);
|
|
45
|
+
index = end + 1;
|
|
46
|
+
regexAllowed = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === "/" && regexAllowed) {
|
|
50
|
+
const end = closingRegex(code, index);
|
|
51
|
+
if (end !== -1) {
|
|
52
|
+
blank(index + 1, end);
|
|
53
|
+
index = end + 1;
|
|
54
|
+
regexAllowed = false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!/\s/.test(char)) regexAllowed = !IDENTIFIER.test(char) && char !== ")" && char !== "]";
|
|
59
|
+
index++;
|
|
60
|
+
}
|
|
61
|
+
return out.join("");
|
|
62
|
+
}
|
|
63
|
+
/** The index of the quote that closes the one at `open`, or the end. */
|
|
64
|
+
function closingQuote(code, open, quote) {
|
|
65
|
+
for (let i = open + 1; i < code.length; i++) {
|
|
66
|
+
if (code[i] === "\\") {
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (code[i] === quote) return i;
|
|
71
|
+
}
|
|
72
|
+
return code.length;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The index of the slash closing a regular expression, or -1 when the
|
|
76
|
+
* slash was division after all.
|
|
77
|
+
*
|
|
78
|
+
* A newline inside means it was not a regular expression, which is the
|
|
79
|
+
* cheap test that keeps `a / b` and `c / d` on one line from swallowing
|
|
80
|
+
* everything between them.
|
|
81
|
+
*/
|
|
82
|
+
function closingRegex(code, open) {
|
|
83
|
+
let inClass = false;
|
|
84
|
+
for (let i = open + 1; i < code.length; i++) {
|
|
85
|
+
const char = code[i];
|
|
86
|
+
if (char === "\\") {
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (char === "\n") return -1;
|
|
91
|
+
if (char === "[") {
|
|
92
|
+
inClass = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === "]") {
|
|
96
|
+
inClass = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === "/" && !inClass) return i;
|
|
100
|
+
}
|
|
101
|
+
return -1;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Every call of `name` in the module, in source order.
|
|
105
|
+
*
|
|
106
|
+
* A bare name has to stand alone: `renderRoot(` matches, `myRenderRoot(`
|
|
107
|
+
* and `thing.renderRoot(` do not, because a method of that name on some
|
|
108
|
+
* other object is not the framework's function. A name written with a
|
|
109
|
+
* leading dot is the opposite and matches only the method call:
|
|
110
|
+
* `.useService(` is how a builder chain reads and the only way it is
|
|
111
|
+
* ever written.
|
|
112
|
+
*/
|
|
113
|
+
function findCalls(code, name, blank = blankLiterals(code)) {
|
|
114
|
+
const member = name.startsWith(".");
|
|
115
|
+
const bare = member ? name.slice(1) : name;
|
|
116
|
+
const sites = [];
|
|
117
|
+
let from = 0;
|
|
118
|
+
for (;;) {
|
|
119
|
+
const at = blank.indexOf(bare, from);
|
|
120
|
+
if (at === -1) return sites;
|
|
121
|
+
from = at + bare.length;
|
|
122
|
+
const before = at === 0 ? "" : blank[at - 1];
|
|
123
|
+
if (member ? before !== "." : before !== "" && (IDENTIFIER.test(before) || before === ".")) continue;
|
|
124
|
+
let cursor = from;
|
|
125
|
+
while (cursor < blank.length && /\s/.test(blank[cursor])) cursor++;
|
|
126
|
+
if (blank[cursor] !== "(") continue;
|
|
127
|
+
const argsEnd = matchingBracket(blank, cursor);
|
|
128
|
+
if (argsEnd === -1) continue;
|
|
129
|
+
sites.push({
|
|
130
|
+
start: at,
|
|
131
|
+
argsStart: cursor + 1,
|
|
132
|
+
argsEnd,
|
|
133
|
+
end: argsEnd + 1
|
|
134
|
+
});
|
|
135
|
+
from = argsEnd + 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** The first call of `name`, or null. */
|
|
139
|
+
function findCall(code, name, blank = blankLiterals(code)) {
|
|
140
|
+
return findCalls(code, name, blank)[0] ?? null;
|
|
141
|
+
}
|
|
142
|
+
/** The index of the bracket closing the one at `open`, or -1. */
|
|
143
|
+
function matchingBracket(blank, open) {
|
|
144
|
+
const stack = [];
|
|
145
|
+
for (let i = open; i < blank.length; i++) {
|
|
146
|
+
const char = blank[i];
|
|
147
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
148
|
+
stack.push(char);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (char === ")" || char === "]" || char === "}") {
|
|
152
|
+
stack.pop();
|
|
153
|
+
if (stack.length === 0) return i;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return -1;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The identifier a call's first argument is, or null when the argument
|
|
160
|
+
* is anything more complicated than a name.
|
|
161
|
+
*
|
|
162
|
+
* `renderRoot(SegueApp)` is the shape every entry in this repository
|
|
163
|
+
* writes and the only one that can be hot-replaced, because a
|
|
164
|
+
* replacement has to be looked up by name in the module that changed.
|
|
165
|
+
* `renderRoot(<App x={1} />)` is legal and gets no HMR wiring, which
|
|
166
|
+
* the plugin says rather than guessing.
|
|
167
|
+
*/
|
|
168
|
+
function firstArgumentName(code, call, blank = blankLiterals(code)) {
|
|
169
|
+
const first = (splitTopLevel(blank.slice(call.argsStart, call.argsEnd))[0] ?? "").trim();
|
|
170
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(first) ? first : null;
|
|
171
|
+
}
|
|
172
|
+
/** Splits an argument list on its top-level commas. */
|
|
173
|
+
function splitTopLevel(text) {
|
|
174
|
+
const parts = [];
|
|
175
|
+
let depth = 0;
|
|
176
|
+
let start = 0;
|
|
177
|
+
for (let i = 0; i < text.length; i++) {
|
|
178
|
+
const char = text[i];
|
|
179
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
180
|
+
else if (char === ")" || char === "]" || char === "}") depth--;
|
|
181
|
+
else if (char === "," && depth === 0) {
|
|
182
|
+
parts.push(text.slice(start, i));
|
|
183
|
+
start = i + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
parts.push(text.slice(start));
|
|
187
|
+
return parts;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Which module each imported name came from: local name to specifier,
|
|
191
|
+
* exactly as the import wrote it.
|
|
192
|
+
*
|
|
193
|
+
* Default and namespace imports are recorded under their local names
|
|
194
|
+
* too, so `import App from './App'` and `import * as app from './App'`
|
|
195
|
+
* both answer. What the map does not carry is whether the binding is a
|
|
196
|
+
* type, because `import type` is stripped before this runs in every
|
|
197
|
+
* configuration that matters and a type never reaches a call.
|
|
198
|
+
*/
|
|
199
|
+
function importSources(code, blank = blankLiterals(code)) {
|
|
200
|
+
const sources = /* @__PURE__ */ new Map();
|
|
201
|
+
const statement = /\bimport\b([\s\S]*?)\bfrom\b\s*(['"])/g;
|
|
202
|
+
for (;;) {
|
|
203
|
+
const match = statement.exec(blank);
|
|
204
|
+
if (match === null) return sources;
|
|
205
|
+
const quote = match[2];
|
|
206
|
+
const open = match.index + match[0].length;
|
|
207
|
+
const close = code.indexOf(quote, open);
|
|
208
|
+
if (close === -1) continue;
|
|
209
|
+
const specifier = code.slice(open, close);
|
|
210
|
+
for (const local of importedNames(match[1])) sources.set(local, specifier);
|
|
211
|
+
statement.lastIndex = close + 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** The local names an import clause binds. */
|
|
215
|
+
function importedNames(clause) {
|
|
216
|
+
const names = [];
|
|
217
|
+
const braces = clause.indexOf("{");
|
|
218
|
+
const head = (braces === -1 ? clause : clause.slice(0, braces)).replace(/\btype\b/g, "");
|
|
219
|
+
for (const part of head.split(",")) {
|
|
220
|
+
const named = /^\s*(?:\*\s*as\s*)?([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
|
|
221
|
+
if (named !== null) names.push(named[1]);
|
|
222
|
+
}
|
|
223
|
+
if (braces !== -1) {
|
|
224
|
+
const close = clause.lastIndexOf("}");
|
|
225
|
+
const list = clause.slice(braces + 1, close === -1 ? clause.length : close);
|
|
226
|
+
for (const part of list.split(",")) {
|
|
227
|
+
const aliased = /^\s*(?:type\s+)?[A-Za-z_$][A-Za-z0-9_$]*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
|
|
228
|
+
const plain = /^\s*(?:type\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(part);
|
|
229
|
+
const name = aliased?.[1] ?? plain?.[1];
|
|
230
|
+
if (name !== void 0) names.push(name);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return names;
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/render.ts
|
|
237
|
+
const MARKER$1 = "/* gesso-vite-plugin: hot replacement */";
|
|
238
|
+
/**
|
|
239
|
+
* Writes the two lines an author would otherwise write by hand.
|
|
240
|
+
*
|
|
241
|
+
* The example below is the specification:
|
|
242
|
+
*
|
|
243
|
+
* const app = renderRoot(LiveApp).useService(LiveFeed);
|
|
244
|
+
* import.meta.hot?.accept('./LiveExampleApp', module => {
|
|
245
|
+
* app.reload(module.LiveApp, [module.LiveFeed]);
|
|
246
|
+
* });
|
|
247
|
+
*
|
|
248
|
+
* Three things have to be found for that to be written mechanically:
|
|
249
|
+
* the handle the chain returns, the module the root component came
|
|
250
|
+
* from, and the modules the services came from. The handle is taken by
|
|
251
|
+
* routing the call through a wrapper of the plugin's own, which keeps
|
|
252
|
+
* the chain that follows it intact whether or not the author assigned
|
|
253
|
+
* it. The modules come from the entry's own import statements, which is
|
|
254
|
+
* exactly the question `importSources` answers.
|
|
255
|
+
*
|
|
256
|
+
* A service declared in a module of its own is accepted alongside the
|
|
257
|
+
* root, because a registry keyed by class object cannot tell a
|
|
258
|
+
* replacement from a stranger and `reload` is where that is repaired.
|
|
259
|
+
* What is deliberately *not* wired is `useRoutes`: routes are read once
|
|
260
|
+
* when the runtime is built, so a change to the route table is a full
|
|
261
|
+
* reload and saying so is better than replacing a tree that still
|
|
262
|
+
* resolves the old routes.
|
|
263
|
+
*/
|
|
264
|
+
function transformRenderWorker(code) {
|
|
265
|
+
if (code.includes(MARKER$1)) return {
|
|
266
|
+
code: null,
|
|
267
|
+
skipped: null
|
|
268
|
+
};
|
|
269
|
+
const blank = blankLiterals(code);
|
|
270
|
+
const imports = importSources(code, blank);
|
|
271
|
+
if (imports.get("renderRoot") !== "gesso-framework") return {
|
|
272
|
+
code: null,
|
|
273
|
+
skipped: null
|
|
274
|
+
};
|
|
275
|
+
const call = findCall(code, "renderRoot", blank);
|
|
276
|
+
if (call === null) return {
|
|
277
|
+
code: null,
|
|
278
|
+
skipped: null
|
|
279
|
+
};
|
|
280
|
+
const root = firstArgumentName(code, call, blank);
|
|
281
|
+
if (root === null) return {
|
|
282
|
+
code: null,
|
|
283
|
+
skipped: "renderRoot() is not called with the name of a component, so there is nothing to look up in a replaced module."
|
|
284
|
+
};
|
|
285
|
+
const rootSource = imports.get(root);
|
|
286
|
+
if (rootSource === void 0) return {
|
|
287
|
+
code: null,
|
|
288
|
+
skipped: `${root} is declared in the worker entry itself rather than imported, so replacing it would replace the entry and reload the page.`
|
|
289
|
+
};
|
|
290
|
+
const services = [];
|
|
291
|
+
for (const site of findCalls(code, ".useService", blank)) {
|
|
292
|
+
const name = firstArgumentName(code, site, blank);
|
|
293
|
+
const source = name === null ? void 0 : imports.get(name);
|
|
294
|
+
if (name !== null && source !== void 0) services.push({
|
|
295
|
+
name,
|
|
296
|
+
source
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const sources = [rootSource, ...services.map((service) => service.source)].filter((source, index, all) => all.indexOf(source) === index);
|
|
300
|
+
const namespace = (source) => `__gessoModule${sources.indexOf(source)}`;
|
|
301
|
+
return {
|
|
302
|
+
code: `${`${code.slice(0, call.start)}__gessoRenderRoot${code.slice(call.start + 10)}`}\n${[
|
|
303
|
+
MARKER$1,
|
|
304
|
+
...sources.map((source, index) => `import * as __gessoImported${index} from ${JSON.stringify(source)};`),
|
|
305
|
+
"function __gessoRenderRoot(root) {",
|
|
306
|
+
" __gessoRenderRoot.app = renderRoot(root);",
|
|
307
|
+
" return __gessoRenderRoot.app;",
|
|
308
|
+
"}",
|
|
309
|
+
`const __gessoLatest = [${sources.map((_, index) => `__gessoImported${index}`).join(", ")}];`,
|
|
310
|
+
"if (import.meta.hot) {",
|
|
311
|
+
` import.meta.hot.accept([${sources.map((source) => JSON.stringify(source)).join(", ")}], replaced => {`,
|
|
312
|
+
" replaced.forEach((module, index) => {",
|
|
313
|
+
" if (module !== undefined) {",
|
|
314
|
+
" __gessoLatest[index] = module;",
|
|
315
|
+
" }",
|
|
316
|
+
" });",
|
|
317
|
+
` __gessoRenderRoot.app.reload(${namespaceRead(namespace(rootSource), root)}, [${services.map((service) => namespaceRead(namespace(service.source), service.name)).join(", ")}]);`,
|
|
318
|
+
" });",
|
|
319
|
+
"}"
|
|
320
|
+
].join("\n")}\n`,
|
|
321
|
+
skipped: null
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/** `__gessoLatest[n].Name`, written through the array so a replacement wins. */
|
|
325
|
+
function namespaceRead(namespaceName, exported) {
|
|
326
|
+
return `__gessoLatest[${namespaceName.slice(13)}].${exported}`;
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/shell.ts
|
|
330
|
+
/** Marks a module the plugin has already rewritten, so it is not done twice. */
|
|
331
|
+
const MARKER = "/* gesso-vite-plugin */";
|
|
332
|
+
/**
|
|
333
|
+
* The `createApp` call in a module, when the module has one that came
|
|
334
|
+
* from the framework.
|
|
335
|
+
*
|
|
336
|
+
* Separate from the transform because finding the worker entries is an
|
|
337
|
+
* asynchronous resolution against the bundler and worth doing only for
|
|
338
|
+
* a module that turns out to be a shell, which is one module in an
|
|
339
|
+
* application.
|
|
340
|
+
*/
|
|
341
|
+
function findShellCall(code, blank = blankLiterals(code)) {
|
|
342
|
+
if (code.includes(MARKER)) return null;
|
|
343
|
+
if (importSources(code, blank).get("createApp") !== "gesso-framework") return null;
|
|
344
|
+
const call = findCall(code, "createApp", blank);
|
|
345
|
+
if (call === null) return null;
|
|
346
|
+
return {
|
|
347
|
+
call,
|
|
348
|
+
needsWorkers: !blank.slice(call.argsStart, call.argsEnd).includes("renderWorker")
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Rewrites the shell module so that it names no worker.
|
|
353
|
+
*
|
|
354
|
+
* The one edit inside the author's own code is the call itself:
|
|
355
|
+
* `createApp(...)` becomes `createApp(__gessoOptions(...))`, on the
|
|
356
|
+
* same line, so every line number in the module is the one it was.
|
|
357
|
+
* Everything else is appended below, as function declarations, which
|
|
358
|
+
* hoist and are therefore usable from the line above them.
|
|
359
|
+
*
|
|
360
|
+
* The merge order is what keeps the promise that the
|
|
361
|
+
* literal construction stays the documented fallback: the plugin's
|
|
362
|
+
* factories go in first and the author's options spread over them, so
|
|
363
|
+
* a `renderWorker` written by hand still wins.
|
|
364
|
+
*
|
|
365
|
+
* Returns null when there is nothing to do, which is the common case:
|
|
366
|
+
* most modules do not call `createApp`.
|
|
367
|
+
*/
|
|
368
|
+
function transformShell(code, options) {
|
|
369
|
+
const shell = findShellCall(code, blankLiterals(code));
|
|
370
|
+
if (shell === null) return null;
|
|
371
|
+
if (options.entries === null && !options.overlay) return null;
|
|
372
|
+
const { call } = shell;
|
|
373
|
+
const args = code.slice(call.argsStart, call.argsEnd).trim();
|
|
374
|
+
return `${`${code.slice(0, call.argsStart)}__gessoOptions(${args === "" ? "{}" : args})${code.slice(call.argsEnd)}`}\n${prelude(options)}`;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The appended half: the worker construction, and in development the
|
|
378
|
+
* error overlay.
|
|
379
|
+
*
|
|
380
|
+
* `new Worker(new URL(...), { type: 'module', ... })` is written out
|
|
381
|
+
* literally because that is the only shape a bundler splits a worker
|
|
382
|
+
* out of, which is the whole reason this text has to be emitted into
|
|
383
|
+
* the author's module rather than living inside the framework. `name`
|
|
384
|
+
* is passed as a member whose value may be undefined, which a
|
|
385
|
+
* dictionary conversion treats as absent, so one construction serves
|
|
386
|
+
* both the named and the unnamed case.
|
|
387
|
+
*/
|
|
388
|
+
function prelude(options) {
|
|
389
|
+
const entries = options.entries;
|
|
390
|
+
const lines = [
|
|
391
|
+
MARKER,
|
|
392
|
+
"function __gessoOptions(options = {}) {",
|
|
393
|
+
" return {",
|
|
394
|
+
...entries === null ? [] : [` renderWorker: () => new Worker(new URL(${JSON.stringify(entries.renderWorker)}, import.meta.url), { type: 'module', name: options.workerName }),`],
|
|
395
|
+
...entries?.appLogicWorker == null ? [] : [` appLogicWorker: () => new Worker(new URL(${JSON.stringify(entries.appLogicWorker)}, import.meta.url), { type: 'module', name: options.workerName }),`],
|
|
396
|
+
...options.overlay ? [" onError: __gessoReportError,"] : [],
|
|
397
|
+
" ...options",
|
|
398
|
+
" };",
|
|
399
|
+
"}"
|
|
400
|
+
];
|
|
401
|
+
return options.overlay ? `${lines.join("\n")}\n${OVERLAY}` : `${lines.join("\n")}\n`;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The development error overlay, loaded the first time something
|
|
405
|
+
* throws and not before.
|
|
406
|
+
*
|
|
407
|
+
* Lazy because it costs nothing until it is needed: a page that never
|
|
408
|
+
* throws never fetches `gesso-devtools`, and a page that does gets
|
|
409
|
+
* the overlay within a network round trip of the error rather than
|
|
410
|
+
* paying for the package on every start.
|
|
411
|
+
*
|
|
412
|
+
* The host is the canvas's parent, which is the element the
|
|
413
|
+
* application was mounted into: the overlay covers the application it
|
|
414
|
+
* is reporting on rather than the viewport, and a Gesso app is not
|
|
415
|
+
* necessarily the whole page. `document.body` is the fallback for an
|
|
416
|
+
* error thrown before there is a canvas at all.
|
|
417
|
+
*
|
|
418
|
+
* State is kept on the function object rather than in module-level
|
|
419
|
+
* `let` bindings, because this block is appended below the code that
|
|
420
|
+
* calls it and a `let` read from above it would be in its temporal
|
|
421
|
+
* dead zone.
|
|
422
|
+
*/
|
|
423
|
+
const OVERLAY = `function __gessoReportError(message, stack, source) {
|
|
424
|
+
const state = (__gessoReportError.state ??= { pending: [], overlay: null, loading: false });
|
|
425
|
+
state.pending.push([message, stack, source]);
|
|
426
|
+
if (state.overlay !== null) {
|
|
427
|
+
__gessoFlushErrors();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (state.loading) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
state.loading = true;
|
|
434
|
+
void import('gesso-devtools').then(({ mountErrorOverlay }) => {
|
|
435
|
+
const canvas = document.querySelector('canvas');
|
|
436
|
+
state.overlay = mountErrorOverlay(canvas?.parentElement ?? document.body);
|
|
437
|
+
__gessoFlushErrors();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function __gessoFlushErrors() {
|
|
441
|
+
const state = __gessoReportError.state;
|
|
442
|
+
for (const [message, stack, source] of state.pending.splice(0)) {
|
|
443
|
+
state.overlay.report(message, stack, source);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
addEventListener('error', event => {
|
|
447
|
+
const error = event.error;
|
|
448
|
+
__gessoReportError(error?.message ?? event.message, error?.stack, 'window');
|
|
449
|
+
});
|
|
450
|
+
addEventListener('unhandledrejection', event => {
|
|
451
|
+
const reason = event.reason;
|
|
452
|
+
__gessoReportError(reason?.message ?? String(reason), reason?.stack, 'window');
|
|
453
|
+
});
|
|
454
|
+
`;
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/index.ts
|
|
457
|
+
/**
|
|
458
|
+
* Where a render worker entry is looked for, beside the shell.
|
|
459
|
+
*
|
|
460
|
+
* Ordered so that a name that says what the file is beats the generic
|
|
461
|
+
* one: an application with both `RenderWorker.ts` and `worker.ts` means
|
|
462
|
+
* the first, and the scaffold, which has only `worker.ts`, still
|
|
463
|
+
* resolves.
|
|
464
|
+
*/
|
|
465
|
+
const RENDER_WORKER_NAMES = [
|
|
466
|
+
"./RenderWorker.ts",
|
|
467
|
+
"./RenderWorker.tsx",
|
|
468
|
+
"./render.worker.ts",
|
|
469
|
+
"./app.render.worker.ts",
|
|
470
|
+
"./worker.ts"
|
|
471
|
+
];
|
|
472
|
+
/** Where an application worker entry is looked for, beside the shell. */
|
|
473
|
+
const APP_WORKER_NAMES = [
|
|
474
|
+
"./AppWorker.ts",
|
|
475
|
+
"./AppWorker.tsx",
|
|
476
|
+
"./app.worker.ts",
|
|
477
|
+
"./app.logic.worker.ts"
|
|
478
|
+
];
|
|
479
|
+
/** Modules the plugin will read: the application's own source, not its dependencies. */
|
|
480
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?)(?:$|\?)/;
|
|
481
|
+
function gesso(options = {}) {
|
|
482
|
+
let serving = false;
|
|
483
|
+
/** The shell module, once one has been transformed. */
|
|
484
|
+
let shellId = null;
|
|
485
|
+
/** The render worker entry, once the shell has named it. */
|
|
486
|
+
let renderWorkerId = null;
|
|
487
|
+
/** Files already reported as reloading, so a save is not a stream of warnings. */
|
|
488
|
+
const reported = /* @__PURE__ */ new Set();
|
|
489
|
+
return {
|
|
490
|
+
name: "gesso",
|
|
491
|
+
enforce: "pre",
|
|
492
|
+
configResolved(config) {
|
|
493
|
+
serving = config.command === "serve";
|
|
494
|
+
},
|
|
495
|
+
async transform(code, id) {
|
|
496
|
+
if (!SOURCE.test(id) || id.includes("/node_modules/")) return null;
|
|
497
|
+
if (serving && options.hmr !== false) {
|
|
498
|
+
const wiring = transformRenderWorker(code);
|
|
499
|
+
if (wiring.skipped !== null) this.warn(`${wiring.skipped} Saving it will reload the page rather than replace the tree.`);
|
|
500
|
+
if (wiring.code !== null) {
|
|
501
|
+
renderWorkerId = id;
|
|
502
|
+
return {
|
|
503
|
+
code: wiring.code,
|
|
504
|
+
map: null
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const call = findShellCall(code);
|
|
509
|
+
if (call === null) return null;
|
|
510
|
+
shellId = id;
|
|
511
|
+
const shell = transformShell(code, {
|
|
512
|
+
entries: call.needsWorkers ? await resolveEntries(this, id, options) : null,
|
|
513
|
+
overlay: serving && options.overlay !== false
|
|
514
|
+
});
|
|
515
|
+
return shell === null ? null : {
|
|
516
|
+
code: shell,
|
|
517
|
+
map: null
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
/**
|
|
521
|
+
* The failure that looks like a bug in the framework.
|
|
522
|
+
*
|
|
523
|
+
* A module reached from the main thread as well
|
|
524
|
+
* as from the render worker cannot be hot-replaced, because Vite
|
|
525
|
+
* propagates the invalidation to a main-thread importer that does
|
|
526
|
+
* not accept and reloads the page. The screen blinks, the app
|
|
527
|
+
* restarts, and nothing anywhere says why. Both ends of the module
|
|
528
|
+
* graph are known here, so the answer is one walk of the importers.
|
|
529
|
+
*/
|
|
530
|
+
hotUpdate({ modules, server }) {
|
|
531
|
+
if (options.diagnostics === false || shellId === null || renderWorkerId === null) return;
|
|
532
|
+
for (const module of modules) {
|
|
533
|
+
if (module.id === null || reported.has(module.id)) continue;
|
|
534
|
+
const importers = importerClosure(module);
|
|
535
|
+
if (!importers.has(shellId) || !importers.has(renderWorkerId)) continue;
|
|
536
|
+
reported.add(module.id);
|
|
537
|
+
server.config.logger.warn(`[gesso] ${short(module.id, server.config.root)} is imported by the main thread as well as by the render worker, so saving it reloads the page instead of replacing the tree. Reach it only from the render worker's own graph to get hot replacement back.`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
/** Every module that transitively imports this one, plus this one. */
|
|
543
|
+
function importerClosure(module) {
|
|
544
|
+
const seen = /* @__PURE__ */ new Set();
|
|
545
|
+
const queue = [module];
|
|
546
|
+
while (queue.length > 0) {
|
|
547
|
+
const next = queue.pop();
|
|
548
|
+
if (next.id === null || seen.has(next.id)) continue;
|
|
549
|
+
seen.add(next.id);
|
|
550
|
+
queue.push(...next.importers);
|
|
551
|
+
}
|
|
552
|
+
return seen;
|
|
553
|
+
}
|
|
554
|
+
/** A path relative to the project, for a message a person reads. */
|
|
555
|
+
function short(id, root) {
|
|
556
|
+
return id.startsWith(root) ? id.slice(root.length + 1) : id;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Finds the worker entries beside a shell module.
|
|
560
|
+
*
|
|
561
|
+
* The names are tried against the bundler's own resolver rather than
|
|
562
|
+
* read off the disk: it already has the answer cached, it applies the
|
|
563
|
+
* project's aliases and extensions, and it is the only thing that can
|
|
564
|
+
* say whether the specifier the plugin is about to write would have
|
|
565
|
+
* resolved. Nothing here touches the filesystem, which is also why the
|
|
566
|
+
* package needs no node types.
|
|
567
|
+
*/
|
|
568
|
+
async function resolveEntries(context, id, options) {
|
|
569
|
+
const renderWorker = await firstResolved(context, id, options.renderWorker === void 0 ? RENDER_WORKER_NAMES : [options.renderWorker]);
|
|
570
|
+
if (renderWorker === null) throw new Error(`gesso-vite-plugin found no render worker beside ${id}. It looked for ${RENDER_WORKER_NAMES.join(", ")}. Name it with the plugin's \`renderWorker\` option, or write \`renderWorker\` in createApp() yourself.`);
|
|
571
|
+
return {
|
|
572
|
+
renderWorker,
|
|
573
|
+
appLogicWorker: options.appLogicWorker === false ? null : await firstResolved(context, id, options.appLogicWorker === void 0 ? APP_WORKER_NAMES : [options.appLogicWorker])
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/** The first specifier that resolves beside `importer`, as written. */
|
|
577
|
+
async function firstResolved(context, importer, names) {
|
|
578
|
+
for (const name of names) if (await context.resolve(name, importer) !== null) return name;
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
582
|
+
export { blankLiterals, gesso as default, gesso, findCall, findCalls, findShellCall, firstArgumentName, importSources, transformRenderWorker, transformShell };
|
|
583
|
+
|
|
584
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["MARKER"],"sources":["../src/source.ts","../src/render.ts","../src/shell.ts","../src/index.ts"],"sourcesContent":["/**\n * Just enough of a JavaScript scanner to find a call and to say which\n * module a name was imported from.\n *\n * Written here rather than taken from npm for the reason the rest of\n * this repository's tooling gives (`packages/devtools/src/sourceMap.ts`,\n * `scripts/gen-layout-fixtures.ts`): the whole of what the plugin needs\n * is \"where does `createApp(` open and close\" and \"where did `SegueApp`\n * come from\", and a parser is a dependency, a TypeScript dialect\n * question and a version to keep in step, in exchange for two\n * questions a scanner answers.\n *\n * The trick that makes it safe is `blankLiterals`: every string,\n * template and comment is replaced by spaces of the same length, so the\n * positions found in the blanked copy are positions in the original,\n * and no brace inside a string can be mistaken for code. Everything\n * below searches the blank copy and slices the original.\n *\n * What it does not do is understand scope. A `createApp` that a module\n * defined for itself would be found the same way an imported one is,\n * which is why the plugin checks the imports first and only then looks\n * for the call.\n */\n\n/** Where a call expression starts, where its arguments are, where it ends. */\nexport interface CallSite {\n /** Index of the first character of the callee name. */\n readonly start: number;\n /** Index just after the opening parenthesis. */\n readonly argsStart: number;\n /** Index of the closing parenthesis. */\n readonly argsEnd: number;\n /** Index just after the closing parenthesis. */\n readonly end: number;\n}\n\nconst IDENTIFIER = /[A-Za-z0-9_$]/;\n\n/**\n * A copy of `code` in which every string, template literal, regular\n * expression body and comment is spaces, and everything else is\n * untouched.\n *\n * Same length as the input, and newlines are kept, so an index into\n * the result is an index into the original and a line number is still\n * a line number. String and template delimiters are kept as well, so a\n * specifier can be found by its quotes here and read from there.\n */\nexport function blankLiterals(code: string): string {\n const out = code.split('');\n let index = 0;\n // What may legally precede a regular expression: an operator, an\n // opening bracket, or the start of the file. After a name, a number\n // or a closing bracket, a slash is division.\n let regexAllowed = true;\n\n const blank = (from: number, to: number): void => {\n for (let i = from; i < to; i++) {\n if (out[i] !== '\\n') {\n out[i] = ' ';\n }\n }\n };\n\n while (index < code.length) {\n const char = code[index];\n if (char === '/' && code[index + 1] === '/') {\n const end = code.indexOf('\\n', index);\n blank(index, end === -1 ? code.length : end);\n index = end === -1 ? code.length : end;\n continue;\n }\n if (char === '/' && code[index + 1] === '*') {\n const end = code.indexOf('*/', index + 2);\n const stop = end === -1 ? code.length : end + 2;\n blank(index, stop);\n index = stop;\n continue;\n }\n if (char === '\"' || char === \"'\") {\n const end = closingQuote(code, index, char);\n blank(index + 1, end);\n index = end + 1;\n regexAllowed = false;\n continue;\n }\n if (char === '`') {\n const end = closingQuote(code, index, '`');\n blank(index + 1, end);\n index = end + 1;\n regexAllowed = false;\n continue;\n }\n if (char === '/' && regexAllowed) {\n const end = closingRegex(code, index);\n if (end !== -1) {\n blank(index + 1, end);\n index = end + 1;\n regexAllowed = false;\n continue;\n }\n }\n if (!/\\s/.test(char)) {\n regexAllowed = !IDENTIFIER.test(char) && char !== ')' && char !== ']';\n }\n index++;\n }\n return out.join('');\n}\n\n/** The index of the quote that closes the one at `open`, or the end. */\nfunction closingQuote(code: string, open: number, quote: string): number {\n for (let i = open + 1; i < code.length; i++) {\n if (code[i] === '\\\\') {\n i++;\n continue;\n }\n if (code[i] === quote) {\n return i;\n }\n }\n return code.length;\n}\n\n/**\n * The index of the slash closing a regular expression, or -1 when the\n * slash was division after all.\n *\n * A newline inside means it was not a regular expression, which is the\n * cheap test that keeps `a / b` and `c / d` on one line from swallowing\n * everything between them.\n */\nfunction closingRegex(code: string, open: number): number {\n let inClass = false;\n for (let i = open + 1; i < code.length; i++) {\n const char = code[i];\n if (char === '\\\\') {\n i++;\n continue;\n }\n if (char === '\\n') {\n return -1;\n }\n if (char === '[') {\n inClass = true;\n continue;\n }\n if (char === ']') {\n inClass = false;\n continue;\n }\n if (char === '/' && !inClass) {\n return i;\n }\n }\n return -1;\n}\n\n/**\n * Every call of `name` in the module, in source order.\n *\n * A bare name has to stand alone: `renderRoot(` matches, `myRenderRoot(`\n * and `thing.renderRoot(` do not, because a method of that name on some\n * other object is not the framework's function. A name written with a\n * leading dot is the opposite and matches only the method call:\n * `.useService(` is how a builder chain reads and the only way it is\n * ever written.\n */\nexport function findCalls(code: string, name: string, blank = blankLiterals(code)): CallSite[] {\n const member = name.startsWith('.');\n const bare = member ? name.slice(1) : name;\n const sites: CallSite[] = [];\n let from = 0;\n for (;;) {\n const at = blank.indexOf(bare, from);\n if (at === -1) {\n return sites;\n }\n from = at + bare.length;\n const before = at === 0 ? '' : blank[at - 1];\n if (member ? before !== '.' : before !== '' && (IDENTIFIER.test(before) || before === '.')) {\n continue;\n }\n let cursor = from;\n while (cursor < blank.length && /\\s/.test(blank[cursor])) {\n cursor++;\n }\n if (blank[cursor] !== '(') {\n continue;\n }\n const argsEnd = matchingBracket(blank, cursor);\n if (argsEnd === -1) {\n continue;\n }\n sites.push({ start: at, argsStart: cursor + 1, argsEnd, end: argsEnd + 1 });\n from = argsEnd + 1;\n }\n}\n\n/** The first call of `name`, or null. */\nexport function findCall(code: string, name: string, blank = blankLiterals(code)): CallSite | null {\n return findCalls(code, name, blank)[0] ?? null;\n}\n\n/** The index of the bracket closing the one at `open`, or -1. */\nexport function matchingBracket(blank: string, open: number): number {\n const stack: string[] = [];\n for (let i = open; i < blank.length; i++) {\n const char = blank[i];\n if (char === '(' || char === '[' || char === '{') {\n stack.push(char);\n continue;\n }\n if (char === ')' || char === ']' || char === '}') {\n stack.pop();\n if (stack.length === 0) {\n return i;\n }\n }\n }\n return -1;\n}\n\n/**\n * The identifier a call's first argument is, or null when the argument\n * is anything more complicated than a name.\n *\n * `renderRoot(SegueApp)` is the shape every entry in this repository\n * writes and the only one that can be hot-replaced, because a\n * replacement has to be looked up by name in the module that changed.\n * `renderRoot(<App x={1} />)` is legal and gets no HMR wiring, which\n * the plugin says rather than guessing.\n */\nexport function firstArgumentName(code: string, call: CallSite, blank = blankLiterals(code)): string | null {\n const first = (splitTopLevel(blank.slice(call.argsStart, call.argsEnd))[0] ?? '').trim();\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(first) ? first : null;\n}\n\n/** Splits an argument list on its top-level commas. */\nfunction splitTopLevel(text: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let start = 0;\n for (let i = 0; i < text.length; i++) {\n const char = text[i];\n if (char === '(' || char === '[' || char === '{') {\n depth++;\n } else if (char === ')' || char === ']' || char === '}') {\n depth--;\n } else if (char === ',' && depth === 0) {\n parts.push(text.slice(start, i));\n start = i + 1;\n }\n }\n parts.push(text.slice(start));\n return parts;\n}\n\n/**\n * Which module each imported name came from: local name to specifier,\n * exactly as the import wrote it.\n *\n * Default and namespace imports are recorded under their local names\n * too, so `import App from './App'` and `import * as app from './App'`\n * both answer. What the map does not carry is whether the binding is a\n * type, because `import type` is stripped before this runs in every\n * configuration that matters and a type never reaches a call.\n */\nexport function importSources(code: string, blank = blankLiterals(code)): Map<string, string> {\n const sources = new Map<string, string>();\n const statement = /\\bimport\\b([\\s\\S]*?)\\bfrom\\b\\s*(['\"])/g;\n for (;;) {\n const match = statement.exec(blank);\n if (match === null) {\n return sources;\n }\n const quote = match[2];\n const open = match.index + match[0].length;\n const close = code.indexOf(quote, open);\n if (close === -1) {\n continue;\n }\n const specifier = code.slice(open, close);\n for (const local of importedNames(match[1])) {\n sources.set(local, specifier);\n }\n statement.lastIndex = close + 1;\n }\n}\n\n/** The local names an import clause binds. */\nfunction importedNames(clause: string): string[] {\n const names: string[] = [];\n const braces = clause.indexOf('{');\n const head = (braces === -1 ? clause : clause.slice(0, braces)).replace(/\\btype\\b/g, '');\n for (const part of head.split(',')) {\n const named = /^\\s*(?:\\*\\s*as\\s*)?([A-Za-z_$][A-Za-z0-9_$]*)\\s*$/.exec(part);\n if (named !== null) {\n names.push(named[1]);\n }\n }\n if (braces !== -1) {\n const close = clause.lastIndexOf('}');\n const list = clause.slice(braces + 1, close === -1 ? clause.length : close);\n for (const part of list.split(',')) {\n const aliased = /^\\s*(?:type\\s+)?[A-Za-z_$][A-Za-z0-9_$]*\\s+as\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*$/.exec(part);\n const plain = /^\\s*(?:type\\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\\s*$/.exec(part);\n const name = aliased?.[1] ?? plain?.[1];\n if (name !== undefined) {\n names.push(name);\n }\n }\n }\n return names;\n}\n","import { blankLiterals, findCall, findCalls, firstArgumentName, importSources } from './source.ts';\n\n/** What the plugin found in a render worker entry, and could wire. */\nexport interface RenderWiring {\n /** The rewritten module, or null when there was nothing to wire. */\n readonly code: string | null;\n /**\n * Why nothing was wired, when nothing was. A sentence for the\n * developer, not an error: an entry the plugin cannot wire still\n * runs, it just full-reloads on a save.\n */\n readonly skipped: string | null;\n}\n\nconst MARKER = '/* gesso-vite-plugin: hot replacement */';\n\n/**\n * Writes the two lines an author would otherwise write by hand.\n *\n * The example below is the specification:\n *\n * const app = renderRoot(LiveApp).useService(LiveFeed);\n * import.meta.hot?.accept('./LiveExampleApp', module => {\n * app.reload(module.LiveApp, [module.LiveFeed]);\n * });\n *\n * Three things have to be found for that to be written mechanically:\n * the handle the chain returns, the module the root component came\n * from, and the modules the services came from. The handle is taken by\n * routing the call through a wrapper of the plugin's own, which keeps\n * the chain that follows it intact whether or not the author assigned\n * it. The modules come from the entry's own import statements, which is\n * exactly the question `importSources` answers.\n *\n * A service declared in a module of its own is accepted alongside the\n * root, because a registry keyed by class object cannot tell a\n * replacement from a stranger and `reload` is where that is repaired.\n * What is deliberately *not* wired is `useRoutes`: routes are read once\n * when the runtime is built, so a change to the route table is a full\n * reload and saying so is better than replacing a tree that still\n * resolves the old routes.\n */\nexport function transformRenderWorker(code: string): RenderWiring {\n if (code.includes(MARKER)) {\n return { code: null, skipped: null };\n }\n const blank = blankLiterals(code);\n const imports = importSources(code, blank);\n if (imports.get('renderRoot') !== 'gesso-framework') {\n return { code: null, skipped: null };\n }\n const call = findCall(code, 'renderRoot', blank);\n if (call === null) {\n return { code: null, skipped: null };\n }\n\n const root = firstArgumentName(code, call, blank);\n if (root === null) {\n return {\n code: null,\n skipped:\n 'renderRoot() is not called with the name of a component, so there is nothing to look up in a replaced module.'\n };\n }\n const rootSource = imports.get(root);\n if (rootSource === undefined) {\n return {\n code: null,\n skipped: `${root} is declared in the worker entry itself rather than imported, so replacing it would replace the entry and reload the page.`\n };\n }\n\n // Every service the chain registers, paired with the module it came\n // from. One declared beside the root is the common case and needs no\n // second dependency; one in a module of its own adds one.\n const services: { name: string; source: string }[] = [];\n for (const site of findCalls(code, '.useService', blank)) {\n const name = firstArgumentName(code, site, blank);\n const source = name === null ? undefined : imports.get(name);\n if (name !== null && source !== undefined) {\n services.push({ name, source });\n }\n }\n\n const sources = [rootSource, ...services.map(service => service.source)].filter(\n (source, index, all) => all.indexOf(source) === index\n );\n const namespace = (source: string): string => `__gessoModule${sources.indexOf(source)}`;\n const rewritten = `${code.slice(0, call.start)}__gessoRenderRoot${code.slice(call.start + 'renderRoot'.length)}`;\n\n const lines = [\n MARKER,\n ...sources.map((source, index) => `import * as __gessoImported${index} from ${JSON.stringify(source)};`),\n 'function __gessoRenderRoot(root) {',\n ' __gessoRenderRoot.app = renderRoot(root);',\n ' return __gessoRenderRoot.app;',\n '}',\n `const __gessoLatest = [${sources.map((_, index) => `__gessoImported${index}`).join(', ')}];`,\n 'if (import.meta.hot) {',\n ` import.meta.hot.accept([${sources.map(source => JSON.stringify(source)).join(', ')}], replaced => {`,\n ' replaced.forEach((module, index) => {',\n ' if (module !== undefined) {',\n ' __gessoLatest[index] = module;',\n ' }',\n ' });',\n ` __gessoRenderRoot.app.reload(${namespaceRead(namespace(rootSource), root)}, [${services\n .map(service => namespaceRead(namespace(service.source), service.name))\n .join(', ')}]);`,\n ' });',\n '}'\n ];\n return { code: `${rewritten}\\n${lines.join('\\n')}\\n`, skipped: null };\n}\n\n/** `__gessoLatest[n].Name`, written through the array so a replacement wins. */\nfunction namespaceRead(namespaceName: string, exported: string): string {\n const index = namespaceName.slice('__gessoModule'.length);\n return `__gessoLatest[${index}].${exported}`;\n}\n","import { blankLiterals, findCall, importSources, type CallSite } from './source.ts';\n\n/** Where the plugin found the two worker entries, as the shell imports them. */\nexport interface WorkerEntries {\n /** A specifier relative to the shell module, e.g. `./RenderWorker.ts`. */\n readonly renderWorker: string;\n /** The same for the application worker, or null when there is none. */\n readonly appLogicWorker: string | null;\n}\n\n/** A `createApp` call the plugin could write for, and what it still needs. */\nexport interface ShellCall {\n readonly call: CallSite;\n /**\n * False when the author wrote `renderWorker` themselves, which is the\n * documented fallback and the case where the plugin has no worker to\n * find. It still wires the overlay in.\n */\n readonly needsWorkers: boolean;\n}\n\nexport interface ShellTransformOptions {\n /** The entries to construct, or null when the author named them. */\n readonly entries: WorkerEntries | null;\n /**\n * Whether to wire the error overlay in as well. True while the dev\n * server is running and never in a build, so a production bundle\n * carries no reference to `gesso-devtools`.\n */\n readonly overlay: boolean;\n}\n\n/** Marks a module the plugin has already rewritten, so it is not done twice. */\nconst MARKER = '/* gesso-vite-plugin */';\n\n/**\n * The `createApp` call in a module, when the module has one that came\n * from the framework.\n *\n * Separate from the transform because finding the worker entries is an\n * asynchronous resolution against the bundler and worth doing only for\n * a module that turns out to be a shell, which is one module in an\n * application.\n */\nexport function findShellCall(code: string, blank = blankLiterals(code)): ShellCall | null {\n if (code.includes(MARKER)) {\n return null;\n }\n if (importSources(code, blank).get('createApp') !== 'gesso-framework') {\n return null;\n }\n const call = findCall(code, 'createApp', blank);\n if (call === null) {\n return null;\n }\n return { call, needsWorkers: !blank.slice(call.argsStart, call.argsEnd).includes('renderWorker') };\n}\n\n/**\n * Rewrites the shell module so that it names no worker.\n *\n * The one edit inside the author's own code is the call itself:\n * `createApp(...)` becomes `createApp(__gessoOptions(...))`, on the\n * same line, so every line number in the module is the one it was.\n * Everything else is appended below, as function declarations, which\n * hoist and are therefore usable from the line above them.\n *\n * The merge order is what keeps the promise that the\n * literal construction stays the documented fallback: the plugin's\n * factories go in first and the author's options spread over them, so\n * a `renderWorker` written by hand still wins.\n *\n * Returns null when there is nothing to do, which is the common case:\n * most modules do not call `createApp`.\n */\nexport function transformShell(code: string, options: ShellTransformOptions): string | null {\n const blank = blankLiterals(code);\n const shell = findShellCall(code, blank);\n if (shell === null) {\n return null;\n }\n if (options.entries === null && !options.overlay) {\n return null;\n }\n const { call } = shell;\n const args = code.slice(call.argsStart, call.argsEnd).trim();\n const rewritten = `${code.slice(0, call.argsStart)}__gessoOptions(${args === '' ? '{}' : args})${code.slice(call.argsEnd)}`;\n return `${rewritten}\\n${prelude(options)}`;\n}\n\n/**\n * The appended half: the worker construction, and in development the\n * error overlay.\n *\n * `new Worker(new URL(...), { type: 'module', ... })` is written out\n * literally because that is the only shape a bundler splits a worker\n * out of, which is the whole reason this text has to be emitted into\n * the author's module rather than living inside the framework. `name`\n * is passed as a member whose value may be undefined, which a\n * dictionary conversion treats as absent, so one construction serves\n * both the named and the unnamed case.\n */\nfunction prelude(options: ShellTransformOptions): string {\n const entries = options.entries;\n const lines = [\n MARKER,\n 'function __gessoOptions(options = {}) {',\n ' return {',\n ...(entries === null\n ? []\n : [\n ` renderWorker: () => new Worker(new URL(${JSON.stringify(entries.renderWorker)}, import.meta.url), { type: 'module', name: options.workerName }),`\n ]),\n ...(entries?.appLogicWorker == null\n ? []\n : [\n ` appLogicWorker: () => new Worker(new URL(${JSON.stringify(entries.appLogicWorker)}, import.meta.url), { type: 'module', name: options.workerName }),`\n ]),\n ...(options.overlay ? [' onError: __gessoReportError,'] : []),\n ' ...options',\n ' };',\n '}'\n ];\n return options.overlay ? `${lines.join('\\n')}\\n${OVERLAY}` : `${lines.join('\\n')}\\n`;\n}\n\n/**\n * The development error overlay, loaded the first time something\n * throws and not before.\n *\n * Lazy because it costs nothing until it is needed: a page that never\n * throws never fetches `gesso-devtools`, and a page that does gets\n * the overlay within a network round trip of the error rather than\n * paying for the package on every start.\n *\n * The host is the canvas's parent, which is the element the\n * application was mounted into: the overlay covers the application it\n * is reporting on rather than the viewport, and a Gesso app is not\n * necessarily the whole page. `document.body` is the fallback for an\n * error thrown before there is a canvas at all.\n *\n * State is kept on the function object rather than in module-level\n * `let` bindings, because this block is appended below the code that\n * calls it and a `let` read from above it would be in its temporal\n * dead zone.\n */\nconst OVERLAY = `function __gessoReportError(message, stack, source) {\n const state = (__gessoReportError.state ??= { pending: [], overlay: null, loading: false });\n state.pending.push([message, stack, source]);\n if (state.overlay !== null) {\n __gessoFlushErrors();\n return;\n }\n if (state.loading) {\n return;\n }\n state.loading = true;\n void import('gesso-devtools').then(({ mountErrorOverlay }) => {\n const canvas = document.querySelector('canvas');\n state.overlay = mountErrorOverlay(canvas?.parentElement ?? document.body);\n __gessoFlushErrors();\n });\n}\nfunction __gessoFlushErrors() {\n const state = __gessoReportError.state;\n for (const [message, stack, source] of state.pending.splice(0)) {\n state.overlay.report(message, stack, source);\n }\n}\naddEventListener('error', event => {\n const error = event.error;\n __gessoReportError(error?.message ?? event.message, error?.stack, 'window');\n});\naddEventListener('unhandledrejection', event => {\n const reason = event.reason;\n __gessoReportError(reason?.message ?? String(reason), reason?.stack, 'window');\n});\n`;\n","import type { EnvironmentModuleNode, Plugin } from 'vite';\n\nimport { transformRenderWorker } from './render.ts';\nimport { findShellCall, transformShell, type WorkerEntries } from './shell.ts';\n\nexport { transformRenderWorker, type RenderWiring } from './render.ts';\nexport {\n findShellCall,\n transformShell,\n type ShellCall,\n type ShellTransformOptions,\n type WorkerEntries\n} from './shell.ts';\nexport { blankLiterals, findCall, findCalls, firstArgumentName, importSources, type CallSite } from './source.ts';\n\n/**\n * `gesso-vite-plugin` — the plumbing between saving a file and seeing\n * the result.\n *\n * A Gesso application is three files either side of a thread barrier\n *, and until now the barrier cost the author four\n * incantations: `new Worker(new URL('./RenderWorker.ts',\n * import.meta.url), { type: 'module' })` twice, because only a literal\n * gets a chunk; `import.meta.hot.accept` with the root module named by\n * hand; and `mountErrorOverlay` wired to `onError`, without which a\n * worker exception reaches a console nobody has selected. None of them\n * is a decision. All of them are written the same way in every\n * application, and getting one wrong fails silently.\n *\n * The three-file shape stays. What this removes is the incantations,\n * and what it adds is the one diagnostic that cannot be read off the\n * screen: a save that reloads the page rather than replacing a module,\n * which traces to the root module having a\n * main-thread importer and which otherwise looks like HMR simply not\n * working.\n *\n * // vite.config.ts\n * import { gesso } from 'gesso-vite-plugin';\n * export default defineConfig({ plugins: [gesso()] });\n *\n * // main.ts\n * createApp({ history: { mode: 'path' } }).mount('#app');\n *\n * The literal construction stays supported and stays documented: the\n * plugin merges its factories *under* the options the author wrote, so\n * a hand-written `renderWorker` wins and an application that would\n * rather say it out loud can.\n */\nexport interface GessoPluginOptions {\n /**\n * The render worker entry, as a specifier the shell module could\n * import. By default the plugin tries the names below, in order, and\n * uses the first that resolves beside the shell.\n */\n readonly renderWorker?: string;\n /**\n * The application worker entry, or false for an application that has\n * none. By default the plugin looks for it and leaves it out when\n * there is nothing to find, which is the single-worker shape the\n * scaffold produces.\n */\n readonly appLogicWorker?: string | false;\n /**\n * Wire `gesso-devtools`'s error overlay into `onError` (default\n * true). Only ever in a dev server: a build carries no reference to\n * the package.\n */\n readonly overlay?: boolean;\n /** Emit the `import.meta.hot.accept` wiring (default true). */\n readonly hmr?: boolean;\n /**\n * Report a save that will reload the page instead of replacing a\n * module (default true).\n */\n readonly diagnostics?: boolean;\n}\n\n/**\n * Where a render worker entry is looked for, beside the shell.\n *\n * Ordered so that a name that says what the file is beats the generic\n * one: an application with both `RenderWorker.ts` and `worker.ts` means\n * the first, and the scaffold, which has only `worker.ts`, still\n * resolves.\n */\nconst RENDER_WORKER_NAMES = [\n './RenderWorker.ts',\n './RenderWorker.tsx',\n './render.worker.ts',\n './app.render.worker.ts',\n './worker.ts'\n];\n\n/** Where an application worker entry is looked for, beside the shell. */\nconst APP_WORKER_NAMES = ['./AppWorker.ts', './AppWorker.tsx', './app.worker.ts', './app.logic.worker.ts'];\n\n/** Modules the plugin will read: the application's own source, not its dependencies. */\nconst SOURCE = /\\.(?:[cm]?[jt]sx?)(?:$|\\?)/;\n\nexport function gesso(options: GessoPluginOptions = {}): Plugin {\n let serving = false;\n /** The shell module, once one has been transformed. */\n let shellId: string | null = null;\n /** The render worker entry, once the shell has named it. */\n let renderWorkerId: string | null = null;\n /** Files already reported as reloading, so a save is not a stream of warnings. */\n const reported = new Set<string>();\n\n return {\n name: 'gesso',\n // Before Vite's own worker and asset plugins, which are what turn\n // the emitted `new Worker(new URL(...))` into a chunk, and before\n // the TypeScript transform, so the module still reads as it was\n // written.\n enforce: 'pre',\n\n configResolved(config) {\n serving = config.command === 'serve';\n },\n\n async transform(code, id) {\n if (!SOURCE.test(id) || id.includes('/node_modules/')) {\n return null;\n }\n\n if (serving && options.hmr !== false) {\n const wiring = transformRenderWorker(code);\n if (wiring.skipped !== null) {\n this.warn(`${wiring.skipped} Saving it will reload the page rather than replace the tree.`);\n }\n if (wiring.code !== null) {\n renderWorkerId = id;\n return { code: wiring.code, map: null };\n }\n }\n\n const call = findShellCall(code);\n if (call === null) {\n return null;\n }\n shellId = id;\n const entries = call.needsWorkers ? await resolveEntries(this, id, options) : null;\n const shell = transformShell(code, { entries, overlay: serving && options.overlay !== false });\n return shell === null ? null : { code: shell, map: null };\n },\n\n /**\n * The failure that looks like a bug in the framework.\n *\n * A module reached from the main thread as well\n * as from the render worker cannot be hot-replaced, because Vite\n * propagates the invalidation to a main-thread importer that does\n * not accept and reloads the page. The screen blinks, the app\n * restarts, and nothing anywhere says why. Both ends of the module\n * graph are known here, so the answer is one walk of the importers.\n */\n hotUpdate({ modules, server }) {\n if (options.diagnostics === false || shellId === null || renderWorkerId === null) {\n return;\n }\n for (const module of modules) {\n if (module.id === null || reported.has(module.id)) {\n continue;\n }\n const importers = importerClosure(module);\n if (!importers.has(shellId) || !importers.has(renderWorkerId)) {\n continue;\n }\n reported.add(module.id);\n server.config.logger.warn(\n `[gesso] ${short(module.id, server.config.root)} is imported by the main thread as well as by the render ` +\n 'worker, so saving it reloads the page instead of replacing the tree. Reach it only from the render ' +\n \"worker's own graph to get hot replacement back.\"\n );\n }\n }\n };\n}\n\nexport default gesso;\n\n/** Every module that transitively imports this one, plus this one. */\nfunction importerClosure(module: EnvironmentModuleNode): Set<string> {\n const seen = new Set<string>();\n const queue = [module];\n while (queue.length > 0) {\n const next = queue.pop()!;\n if (next.id === null || seen.has(next.id)) {\n continue;\n }\n seen.add(next.id);\n queue.push(...next.importers);\n }\n return seen;\n}\n\n/** A path relative to the project, for a message a person reads. */\nfunction short(id: string, root: string): string {\n return id.startsWith(root) ? id.slice(root.length + 1) : id;\n}\n\n/** What the plugin resolves against, so a spec can hand it a fake. */\ninterface Resolver {\n resolve(source: string, importer: string): Promise<{ id: string } | null>;\n}\n\n/**\n * Finds the worker entries beside a shell module.\n *\n * The names are tried against the bundler's own resolver rather than\n * read off the disk: it already has the answer cached, it applies the\n * project's aliases and extensions, and it is the only thing that can\n * say whether the specifier the plugin is about to write would have\n * resolved. Nothing here touches the filesystem, which is also why the\n * package needs no node types.\n */\nasync function resolveEntries(context: Resolver, id: string, options: GessoPluginOptions): Promise<WorkerEntries> {\n const renderWorker = await firstResolved(\n context,\n id,\n options.renderWorker === undefined ? RENDER_WORKER_NAMES : [options.renderWorker]\n );\n if (renderWorker === null) {\n throw new Error(\n `gesso-vite-plugin found no render worker beside ${id}. It looked for ${RENDER_WORKER_NAMES.join(', ')}. ` +\n \"Name it with the plugin's `renderWorker` option, or write `renderWorker` in createApp() yourself.\"\n );\n }\n const appLogicWorker =\n options.appLogicWorker === false\n ? null\n : await firstResolved(\n context,\n id,\n options.appLogicWorker === undefined ? APP_WORKER_NAMES : [options.appLogicWorker]\n );\n return { renderWorker, appLogicWorker };\n}\n\n/** The first specifier that resolves beside `importer`, as written. */\nasync function firstResolved(context: Resolver, importer: string, names: readonly string[]): Promise<string | null> {\n for (const name of names) {\n if ((await context.resolve(name, importer)) !== null) {\n return name;\n }\n }\n return null;\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gesso-vite-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Finds a Gesso application's worker entries, writes the constructions and the hot-replacement wiring, and says when a save will reload the page.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Kevin Baker",
|
|
7
|
+
"homepage": "https://github.com/kevinpbaker/gesso#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kevinpbaker/gesso.git",
|
|
11
|
+
"directory": "packages/vite-plugin"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/kevinpbaker/gesso/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"gesso",
|
|
18
|
+
"canvas",
|
|
19
|
+
"ui",
|
|
20
|
+
"typescript",
|
|
21
|
+
"vite",
|
|
22
|
+
"vite-plugin",
|
|
23
|
+
"hmr",
|
|
24
|
+
"web-worker",
|
|
25
|
+
"build-tool"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"README.md",
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"dist",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"vite": "^8.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"vite": "^8.2.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown"
|
|
52
|
+
}
|
|
53
|
+
}
|