@nuvio/vite-plugin 0.5.4 → 1.0.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.
@@ -0,0 +1,482 @@
1
+ import {
2
+ buildSourceIndex,
3
+ detectProjectLibraries,
4
+ extractIdsFromSource,
5
+ handleTagElementMessage,
6
+ pathnameFromUpgradeUrl,
7
+ pickBestSourceIndex,
8
+ readRuntimeVersions,
9
+ resolvePatchClassNameMode
10
+ } from "./chunk-QAQAJG7B.js";
11
+
12
+ // src/nuvio-dev-session.ts
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { watch } from "fs";
16
+ import { WebSocket, WebSocketServer } from "ws";
17
+ import { applyPatchToSource } from "@nuvio/ast-engine";
18
+ import {
19
+ NUVIO_WS_PATH,
20
+ PROTOCOL_VERSION,
21
+ parseClientMessage,
22
+ serializeServerMessage
23
+ } from "@nuvio/shared";
24
+ import { assertPathWithinRoot } from "@nuvio/shared/secure-path";
25
+ var APP_ENTRY_CANDIDATES = ["src/App.tsx", "src/app.tsx", "App.tsx"];
26
+ var DEFAULT_GLOBS = [
27
+ "src/**/*.{tsx,jsx}",
28
+ "apps/**/src/**/*.{tsx,jsx}",
29
+ "packages/**/src/**/*.{tsx,jsx}"
30
+ ];
31
+ function nuvioWsMessageToText(data) {
32
+ if (typeof data === "string") {
33
+ return data;
34
+ }
35
+ if (Buffer.isBuffer(data)) {
36
+ return data.toString("utf8");
37
+ }
38
+ if (data instanceof ArrayBuffer) {
39
+ return Buffer.from(data).toString("utf8");
40
+ }
41
+ if (ArrayBuffer.isView(data)) {
42
+ const v = data;
43
+ return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("utf8");
44
+ }
45
+ return String(data);
46
+ }
47
+ function isAllowedOrigin(origin) {
48
+ if (origin === void 0 || origin === "") {
49
+ return true;
50
+ }
51
+ try {
52
+ const u = new URL(origin);
53
+ return (u.hostname === "localhost" || u.hostname === "127.0.0.1") && (u.protocol === "http:" || u.protocol === "https:");
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ function supplementIndexFromAppTsx(serverRoot, built, classNameMode, emitWarn) {
59
+ if (built.entries.length > 0) {
60
+ return built;
61
+ }
62
+ for (const rel of APP_ENTRY_CANDIDATES) {
63
+ const appTsx = path.resolve(serverRoot, rel);
64
+ if (!fs.existsSync(appTsx)) {
65
+ continue;
66
+ }
67
+ try {
68
+ const code = fs.readFileSync(appTsx, "utf8");
69
+ const hits = extractIdsFromSource(appTsx, code, { classNameMode });
70
+ if (hits.length === 0) {
71
+ continue;
72
+ }
73
+ emitWarn(
74
+ `[Nuvio] Source index had 0 ids; supplemented from ${appTsx} (${hits.length} id(s)).`
75
+ );
76
+ return {
77
+ ...built,
78
+ entries: hits,
79
+ scannedFileCount: Math.max(built.scannedFileCount, 1)
80
+ };
81
+ } catch {
82
+ }
83
+ }
84
+ return built;
85
+ }
86
+ function attachNuvioDevSession(httpServer, options) {
87
+ const log = options.log ?? console;
88
+ const enabled = options.enabled ?? process.env.NUVIO !== "0";
89
+ const scanGlobs = options.scanGlobs ?? DEFAULT_GLOBS;
90
+ const verbose = options.verbose ?? false;
91
+ const classNameMode = options.classNameMode ?? "literal-only";
92
+ const serverRoot = path.resolve(options.root);
93
+ const fromConfigFile = options.configDir ?? "";
94
+ const rootCandidates = [path.resolve(fromConfigFile || serverRoot), serverRoot, process.cwd()];
95
+ const rootsLabel = [...new Set(rootCandidates)].join(" | ");
96
+ let indexVersion = 0;
97
+ let cachedIndexPayload = null;
98
+ let runtimeDiagnostics = { overlayCssMode: "self-contained" };
99
+ const idToEntry = /* @__PURE__ */ new Map();
100
+ let lastDuplicateErrors = [];
101
+ const wss = new WebSocketServer({ noServer: true });
102
+ const undoStack = [];
103
+ const UNDO_MAX = 32;
104
+ const pushUndoSnapshot = (file, contents) => {
105
+ undoStack.push({ file, contents });
106
+ while (undoStack.length > UNDO_MAX) {
107
+ undoStack.shift();
108
+ }
109
+ };
110
+ const rebuildIndex = () => {
111
+ if (!enabled) {
112
+ return;
113
+ }
114
+ const detectedLibraries = detectProjectLibraries(serverRoot);
115
+ const indexOptions = { classNameMode, detectedLibraries };
116
+ let built = pickBestSourceIndex(rootCandidates, scanGlobs, indexOptions);
117
+ built = supplementIndexFromAppTsx(serverRoot, built, classNameMode, log.warn);
118
+ if (built.entries.length === 0) {
119
+ const fallback = buildSourceIndex(serverRoot, ["src/**/*.{tsx,jsx}"], indexOptions);
120
+ if (fallback.entries.length > 0) {
121
+ log.warn(
122
+ `[Nuvio] Multi-root scan yielded 0 ids; using serverRoot-only index (${fallback.entries.length} id(s)).`
123
+ );
124
+ built = fallback;
125
+ }
126
+ }
127
+ indexVersion += 1;
128
+ idToEntry.clear();
129
+ for (const e of built.entries) {
130
+ idToEntry.set(e.id, e);
131
+ }
132
+ lastDuplicateErrors = built.duplicateErrors;
133
+ runtimeDiagnostics = {
134
+ ...readRuntimeVersions(serverRoot),
135
+ overlayCssMode: "self-contained",
136
+ detectedLibraries: detectedLibraries.length > 0 ? detectedLibraries : void 0
137
+ };
138
+ cachedIndexPayload = serializeServerMessage({
139
+ type: "indexReady",
140
+ protocolVersion: PROTOCOL_VERSION,
141
+ indexVersion,
142
+ entries: built.entries,
143
+ duplicateErrors: built.duplicateErrors,
144
+ diagnostics: runtimeDiagnostics
145
+ });
146
+ if (verbose) {
147
+ log.info(
148
+ `[Nuvio] index roots=${rootsLabel} matchedFiles=${built.scannedFileCount} uniqueIds=${built.entries.length}`
149
+ );
150
+ } else {
151
+ log.info(`[Nuvio] index \u2014 ${built.entries.length} id(s), ${built.scannedFileCount} file(s)`);
152
+ }
153
+ if (cachedIndexPayload && wss.clients.size > 0) {
154
+ for (const client of wss.clients) {
155
+ if (client.readyState === WebSocket.OPEN) {
156
+ client.send(cachedIndexPayload);
157
+ }
158
+ }
159
+ }
160
+ };
161
+ const debouncedRebuild = /* @__PURE__ */ (() => {
162
+ let t;
163
+ return () => {
164
+ if (t) {
165
+ clearTimeout(t);
166
+ }
167
+ t = setTimeout(() => {
168
+ rebuildIndex();
169
+ t = void 0;
170
+ }, 120);
171
+ };
172
+ })();
173
+ const srcDir = path.join(serverRoot, "src");
174
+ let fileWatcher = null;
175
+ if (enabled && fs.existsSync(srcDir)) {
176
+ try {
177
+ fileWatcher = watch(srcDir, { recursive: true }, (_event, filename) => {
178
+ if (filename && /\.(tsx|jsx)$/.test(filename)) {
179
+ debouncedRebuild();
180
+ }
181
+ });
182
+ } catch {
183
+ }
184
+ }
185
+ wss.on("connection", (ws) => {
186
+ if (cachedIndexPayload && ws.readyState === WebSocket.OPEN) {
187
+ ws.send(cachedIndexPayload);
188
+ }
189
+ ws.on("message", async (data) => {
190
+ const text = nuvioWsMessageToText(data);
191
+ const msg = parseClientMessage(text);
192
+ if (!msg) {
193
+ ws.send(
194
+ serializeServerMessage({
195
+ type: "error",
196
+ code: "bad_message",
197
+ message: "Invalid client message"
198
+ })
199
+ );
200
+ return;
201
+ }
202
+ if (msg.protocolVersion !== PROTOCOL_VERSION) {
203
+ ws.send(
204
+ serializeServerMessage({
205
+ type: "error",
206
+ code: "bad_version",
207
+ message: `Expected protocolVersion ${PROTOCOL_VERSION}`,
208
+ requestId: "requestId" in msg ? msg.requestId : void 0
209
+ })
210
+ );
211
+ return;
212
+ }
213
+ if (msg.type === "ping") {
214
+ ws.send(
215
+ serializeServerMessage({
216
+ type: "pong",
217
+ protocolVersion: PROTOCOL_VERSION,
218
+ requestId: msg.requestId,
219
+ diagnostics: runtimeDiagnostics
220
+ })
221
+ );
222
+ if (cachedIndexPayload) {
223
+ ws.send(cachedIndexPayload);
224
+ }
225
+ return;
226
+ }
227
+ if (msg.type === "select") {
228
+ const entry = idToEntry.get(msg.id);
229
+ if (!entry) {
230
+ ws.send(
231
+ serializeServerMessage({
232
+ type: "selectAck",
233
+ protocolVersion: PROTOCOL_VERSION,
234
+ requestId: msg.requestId,
235
+ id: msg.id,
236
+ ok: false,
237
+ errorCode: "unknown_id",
238
+ errorMessage: "Id not found in dev source index"
239
+ })
240
+ );
241
+ return;
242
+ }
243
+ ws.send(
244
+ serializeServerMessage({
245
+ type: "selectAck",
246
+ protocolVersion: PROTOCOL_VERSION,
247
+ requestId: msg.requestId,
248
+ id: msg.id,
249
+ ok: true,
250
+ file: entry.file,
251
+ line: entry.line,
252
+ column: entry.column,
253
+ patchHostId: entry.patchHostId,
254
+ primaryTextTargetKey: entry.primaryTextTargetKey,
255
+ textTargets: entry.textTargets,
256
+ styleTargets: entry.styleTargets,
257
+ hierarchyRole: entry.hierarchyRole,
258
+ parentHostId: entry.parentHostId,
259
+ childTargetIds: entry.childTargetIds,
260
+ rowTargets: entry.rowTargets,
261
+ tableMeta: entry.tableMeta,
262
+ tableDataField: entry.tableDataField
263
+ })
264
+ );
265
+ return;
266
+ }
267
+ if (msg.type === "tagElement") {
268
+ const writeGuardRoot = path.resolve(fromConfigFile || serverRoot);
269
+ await handleTagElementMessage(ws, msg, {
270
+ writeGuardRoot,
271
+ projectRoot: writeGuardRoot,
272
+ idToEntry,
273
+ duplicateIds: lastDuplicateErrors,
274
+ onIndexRebuilt: debouncedRebuild
275
+ });
276
+ return;
277
+ }
278
+ if (msg.type === "patchUndo") {
279
+ const writeGuardRoot = path.resolve(fromConfigFile || serverRoot);
280
+ const last = undoStack.pop();
281
+ if (!last) {
282
+ ws.send(
283
+ serializeServerMessage({
284
+ type: "patchUndoAck",
285
+ protocolVersion: PROTOCOL_VERSION,
286
+ requestId: msg.requestId,
287
+ ok: false,
288
+ errorCode: "empty_stack",
289
+ errorMessage: "Nothing to undo"
290
+ })
291
+ );
292
+ return;
293
+ }
294
+ try {
295
+ assertPathWithinRoot(writeGuardRoot, last.file);
296
+ fs.writeFileSync(last.file, last.contents, "utf8");
297
+ } catch (e) {
298
+ undoStack.push(last);
299
+ ws.send(
300
+ serializeServerMessage({
301
+ type: "patchUndoAck",
302
+ protocolVersion: PROTOCOL_VERSION,
303
+ requestId: msg.requestId,
304
+ ok: false,
305
+ errorCode: "undo_write_error",
306
+ errorMessage: String(e)
307
+ })
308
+ );
309
+ return;
310
+ }
311
+ ws.send(
312
+ serializeServerMessage({
313
+ type: "patchUndoAck",
314
+ protocolVersion: PROTOCOL_VERSION,
315
+ requestId: msg.requestId,
316
+ ok: true,
317
+ file: last.file,
318
+ undoStackDepth: undoStack.length
319
+ })
320
+ );
321
+ return;
322
+ }
323
+ if (msg.type === "patchApply") {
324
+ const entry = idToEntry.get(msg.id);
325
+ const writeGuardRoot = path.resolve(fromConfigFile || serverRoot);
326
+ const dryRun = msg.dryRun === true;
327
+ const patchAckExtras = dryRun ? { dryRun: true } : {};
328
+ if (!entry) {
329
+ ws.send(
330
+ serializeServerMessage({
331
+ type: "patchAck",
332
+ protocolVersion: PROTOCOL_VERSION,
333
+ requestId: msg.requestId,
334
+ id: msg.id,
335
+ ok: false,
336
+ errorCode: "unknown_id",
337
+ errorMessage: "Id not found in dev source index",
338
+ ...patchAckExtras
339
+ })
340
+ );
341
+ return;
342
+ }
343
+ try {
344
+ assertPathWithinRoot(writeGuardRoot, entry.file);
345
+ } catch (e) {
346
+ ws.send(
347
+ serializeServerMessage({
348
+ type: "patchAck",
349
+ protocolVersion: PROTOCOL_VERSION,
350
+ requestId: msg.requestId,
351
+ id: msg.id,
352
+ ok: false,
353
+ errorCode: "path_escape",
354
+ errorMessage: String(e),
355
+ ...patchAckExtras
356
+ })
357
+ );
358
+ return;
359
+ }
360
+ let source;
361
+ try {
362
+ source = fs.readFileSync(entry.file, "utf8");
363
+ } catch (e) {
364
+ ws.send(
365
+ serializeServerMessage({
366
+ type: "patchAck",
367
+ protocolVersion: PROTOCOL_VERSION,
368
+ requestId: msg.requestId,
369
+ id: msg.id,
370
+ ok: false,
371
+ errorCode: "read_error",
372
+ errorMessage: String(e),
373
+ ...patchAckExtras
374
+ })
375
+ );
376
+ return;
377
+ }
378
+ const result = await applyPatchToSource(source, entry.file, msg.id, msg.ops, {
379
+ classNameMode: resolvePatchClassNameMode(entry, classNameMode),
380
+ activeBreakpoint: msg.activeBreakpoint
381
+ });
382
+ if (!result.ok) {
383
+ ws.send(
384
+ serializeServerMessage({
385
+ type: "patchAck",
386
+ protocolVersion: PROTOCOL_VERSION,
387
+ requestId: msg.requestId,
388
+ id: msg.id,
389
+ ok: false,
390
+ errorCode: result.code,
391
+ errorMessage: result.message,
392
+ ...patchAckExtras
393
+ })
394
+ );
395
+ return;
396
+ }
397
+ if (dryRun) {
398
+ ws.send(
399
+ serializeServerMessage({
400
+ type: "patchAck",
401
+ protocolVersion: PROTOCOL_VERSION,
402
+ requestId: msg.requestId,
403
+ id: msg.id,
404
+ ok: true,
405
+ diffSummary: result.diffSummary,
406
+ dryRun: true
407
+ })
408
+ );
409
+ return;
410
+ }
411
+ try {
412
+ fs.writeFileSync(entry.file, result.source, "utf8");
413
+ } catch (e) {
414
+ ws.send(
415
+ serializeServerMessage({
416
+ type: "patchAck",
417
+ protocolVersion: PROTOCOL_VERSION,
418
+ requestId: msg.requestId,
419
+ id: msg.id,
420
+ ok: false,
421
+ errorCode: "write_error",
422
+ errorMessage: String(e)
423
+ })
424
+ );
425
+ return;
426
+ }
427
+ pushUndoSnapshot(entry.file, source);
428
+ ws.send(
429
+ serializeServerMessage({
430
+ type: "patchAck",
431
+ protocolVersion: PROTOCOL_VERSION,
432
+ requestId: msg.requestId,
433
+ id: msg.id,
434
+ ok: true,
435
+ diffSummary: result.diffSummary,
436
+ writtenFile: entry.file,
437
+ undoStackDepth: undoStack.length
438
+ })
439
+ );
440
+ }
441
+ });
442
+ });
443
+ const onUpgrade = (request, socket, head) => {
444
+ if (!enabled) {
445
+ return;
446
+ }
447
+ const pathname = pathnameFromUpgradeUrl(request.url);
448
+ if (pathname !== NUVIO_WS_PATH) {
449
+ return;
450
+ }
451
+ if (!isAllowedOrigin(request.headers.origin)) {
452
+ socket.destroy();
453
+ return;
454
+ }
455
+ wss.handleUpgrade(request, socket, head, (ws) => {
456
+ wss.emit("connection", ws, request);
457
+ });
458
+ };
459
+ httpServer.on("upgrade", onUpgrade);
460
+ if (enabled) {
461
+ log.info("[Nuvio] dev session attached (App Router / custom server mode)");
462
+ rebuildIndex();
463
+ } else {
464
+ log.info("[Nuvio] disabled (set NUVIO=1 to enable)");
465
+ }
466
+ return {
467
+ rebuildIndex,
468
+ close: () => {
469
+ httpServer.off("upgrade", onUpgrade);
470
+ fileWatcher?.close();
471
+ for (const client of wss.clients) {
472
+ client.close();
473
+ }
474
+ wss.close();
475
+ }
476
+ };
477
+ }
478
+
479
+ export {
480
+ DEFAULT_GLOBS,
481
+ attachNuvioDevSession
482
+ };