@nativescript/vite 0.0.1-alpha.1 → 0.0.1-alpha.2
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/dist/configuration/base.js +134 -36
- package/dist/helpers/external-configs.js +1 -1
- package/dist/helpers/global-defines.d.ts +1 -0
- package/dist/helpers/global-defines.js +2 -0
- package/dist/helpers/main-entry-hmr-includes.d.ts +1 -0
- package/dist/helpers/main-entry-hmr-includes.js +18 -0
- package/dist/helpers/main-entry.d.ts +2 -2
- package/dist/helpers/main-entry.js +58 -94
- package/dist/helpers/module-runner-patch.d.ts +3 -0
- package/dist/helpers/module-runner-patch.js +65 -0
- package/dist/helpers/ns-cli-plugins.d.ts +1 -14
- package/dist/helpers/ns-cli-plugins.js +54 -107
- package/dist/helpers/package-platform-aliases.d.ts +1 -1
- package/dist/helpers/package-platform-aliases.js +4 -4
- package/dist/helpers/ts-config-paths.d.ts +1 -1
- package/dist/helpers/ts-config-paths.js +3 -3
- package/dist/hmr/client-vue.d.ts +6 -0
- package/dist/hmr/client-vue.js +563 -0
- package/dist/hmr/component-tracker.d.ts +23 -0
- package/dist/hmr/component-tracker.js +193 -0
- package/dist/hmr/css-handler.d.ts +4 -0
- package/dist/hmr/css-handler.js +77 -0
- package/dist/hmr/message-handler.d.ts +1 -0
- package/dist/hmr/message-handler.js +590 -0
- package/dist/hmr/nsv-hooks.d.ts +2 -0
- package/dist/hmr/nsv-hooks.js +481 -0
- package/dist/hmr/plugin-vue.d.ts +2 -0
- package/dist/hmr/plugin-vue.js +38 -0
- package/dist/hmr/plugins/index.d.ts +1 -0
- package/dist/hmr/plugins/index.js +16 -0
- package/dist/hmr/plugins/plugin-vue.d.ts +2 -0
- package/dist/hmr/plugins/plugin-vue.js +41 -0
- package/dist/hmr/plugins/websocket-vue.d.ts +2 -0
- package/dist/hmr/plugins/websocket-vue.js +882 -0
- package/dist/hmr/runtime-vue.d.ts +13 -0
- package/dist/hmr/runtime-vue.js +2306 -0
- package/dist/hmr/types.d.ts +24 -0
- package/dist/hmr/types.js +2 -0
- package/dist/hmr/websocket-vue.d.ts +2 -0
- package/dist/hmr/websocket-vue.js +875 -0
- package/dist/shims/node-module.d.ts +5 -0
- package/dist/shims/node-module.js +12 -0
- package/package.json +2 -1
- package/dist/configuration/old-without-merge-base.d.ts +0 -13
- package/dist/configuration/old-without-merge-base.js +0 -249
- package/dist/helpers/side-effects.d.ts +0 -14
- package/dist/helpers/side-effects.js +0 -69
- package/dist/hmr/hmr-angular.d.ts +0 -1
- package/dist/hmr/hmr-angular.js +0 -34
- package/dist/hmr/hmr-bridge.d.ts +0 -18
- package/dist/hmr/hmr-bridge.js +0 -154
- package/dist/hmr/hmr-client.d.ts +0 -5
- package/dist/hmr/hmr-client.js +0 -93
- package/dist/hmr/hmr-server.d.ts +0 -20
- package/dist/hmr/hmr-server.js +0 -179
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
// Helper: remove balanced `if (import.meta.hot) { ... }` blocks to avoid orphan fragments
|
|
6
|
+
function stripImportMetaHotBlocks(input) {
|
|
7
|
+
let out = input;
|
|
8
|
+
const re = /if\s*\(\s*import\.meta\.hot\s*\)\s*\{/g;
|
|
9
|
+
let m;
|
|
10
|
+
while ((m = re.exec(out)) !== null) {
|
|
11
|
+
let start = m.index;
|
|
12
|
+
let i = start + m[0].length;
|
|
13
|
+
let depth = 1;
|
|
14
|
+
while (i < out.length && depth > 0) {
|
|
15
|
+
const ch = out[i++];
|
|
16
|
+
if (ch === "{")
|
|
17
|
+
depth++;
|
|
18
|
+
else if (ch === "}")
|
|
19
|
+
depth--;
|
|
20
|
+
}
|
|
21
|
+
// Replace the whole balanced block with a comment
|
|
22
|
+
out =
|
|
23
|
+
out.slice(0, start) + "// Removed import.meta.hot block\n" + out.slice(i);
|
|
24
|
+
re.lastIndex = start; // reset search after modification
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
// Helper: remove common Vue HMR runtime statements and orphan else blocks
|
|
29
|
+
function stripVueHmrRuntimeNoise(input) {
|
|
30
|
+
let out = input;
|
|
31
|
+
// Remove standalone createRecord/reload/rerender calls
|
|
32
|
+
out = out.replace(/__VUE_HMR_RUNTIME__\.(?:createRecord|reload|rerender)\s*\([\s\S]*?\);\s*/g, "");
|
|
33
|
+
// Remove patterns like `} else { __VUE_HMR_RUNTIME__.reload(...); }`
|
|
34
|
+
out = out.replace(/\}\s*else\s*\{\s*__VUE_HMR_RUNTIME__\.(?:reload|rerender)\s*\([\s\S]*?\);\s*\}\s*/g, "");
|
|
35
|
+
// Remove orphan `else { __VUE_HMR_RUNTIME__... }` if the preceding `if (import.meta.hot) {}` was stripped
|
|
36
|
+
out = out.replace(/(?:^|\n)\s*else\s*\{\s*__VUE_HMR_RUNTIME__\.(?:reload|rerender)\s*\([\s\S]*?\);\s*\}\s*/g, "\n");
|
|
37
|
+
// Collapse any now-empty `else {}` blocks
|
|
38
|
+
out = out.replace(/\}\s*else\s*\{\s*\}\s*/g, "}\n");
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
// Helper: Strip anything between the compiled HMR id assignment and the export default
|
|
42
|
+
// This nukes any remaining HMR scaffolding the compiler injects and avoids syntax drift
|
|
43
|
+
function stripVueHmrTail(input) {
|
|
44
|
+
let out = input;
|
|
45
|
+
// Match from a line containing `.__hmrId = "...";` up to but not including the `export default` line
|
|
46
|
+
out = out.replace(/\n[^\n]*__hmrId\s*=\s*['\"][^'\"]+['\"];[\s\S]*?(?=\n\s*export\s+default)/g, (match) => {
|
|
47
|
+
// Keep just a single newline to preserve spacing before export
|
|
48
|
+
return "\n";
|
|
49
|
+
});
|
|
50
|
+
// Also guard against any stray `typeof __VUE_HMR_RUNTIME__ !== "undefined" && ...` lines left over
|
|
51
|
+
out = out.replace(/^[^\n]*typeof\s+__VUE_HMR_RUNTIME__[^\n]*$/gm, "");
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
// Helper: remove named specifiers from import curly lists; drop import if empty
|
|
55
|
+
function removeNamedImports(code, namesToRemove) {
|
|
56
|
+
const re = /^(\s*import\s*\{)([^}]*)(\}\s*from\s*['"][^'"]+['"];?)/gm;
|
|
57
|
+
return code.replace(re, (_m, p1, specList, p3) => {
|
|
58
|
+
const remaining = specList
|
|
59
|
+
.split(",")
|
|
60
|
+
.map((s) => s.trim())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.filter((entry) => {
|
|
63
|
+
// entry like: name OR name as alias
|
|
64
|
+
const base = entry.split(/\s+as\s+/i)[0].trim();
|
|
65
|
+
return !namesToRemove.includes(base);
|
|
66
|
+
});
|
|
67
|
+
if (!remaining.length)
|
|
68
|
+
return "";
|
|
69
|
+
return `${p1} ${remaining.join(", ")} ${p3}`;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Helper: inject const bindings from globals for the given names
|
|
73
|
+
function injectGlobalsBindings(code, names) {
|
|
74
|
+
if (!names.length)
|
|
75
|
+
return code;
|
|
76
|
+
const lines = names.map((n) => `const ${n} = globalThis.${n};`);
|
|
77
|
+
return lines.join("\n") + "\n" + code;
|
|
78
|
+
}
|
|
79
|
+
export function hmrWebSocketVue(verboseLogs = false) {
|
|
80
|
+
let wss = null;
|
|
81
|
+
// Map of project-relative vue path -> device-local hashed filename (e.g., sfc-<hash>.mjs)
|
|
82
|
+
const sfcFileMap = new Map();
|
|
83
|
+
let registrySent = false;
|
|
84
|
+
return {
|
|
85
|
+
name: "pure-vue-websocket",
|
|
86
|
+
apply: "serve",
|
|
87
|
+
configureServer(server) {
|
|
88
|
+
const httpServer = server.httpServer;
|
|
89
|
+
if (!httpServer)
|
|
90
|
+
return;
|
|
91
|
+
wss = new WebSocketServer({ noServer: true, path: "/ns-hmr" });
|
|
92
|
+
if (verboseLogs)
|
|
93
|
+
console.log("[pure-vue-ws] WebSocket server configured on /ns-hmr");
|
|
94
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
95
|
+
const pathname = new URL(request.url || "", "http://localhost")
|
|
96
|
+
.pathname;
|
|
97
|
+
if (pathname === "/ns-hmr") {
|
|
98
|
+
wss?.handleUpgrade(request, socket, head, (ws) => {
|
|
99
|
+
wss?.emit("connection", ws, request);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
wss.on("connection", async (ws) => {
|
|
104
|
+
if (verboseLogs)
|
|
105
|
+
console.log("[pure-vue-ws] Client connected to /ns-hmr");
|
|
106
|
+
ws.on("close", () => verboseLogs && console.log("[pure-vue-ws] Client disconnected"));
|
|
107
|
+
// Send SFC registry once on first client connection
|
|
108
|
+
try {
|
|
109
|
+
if (!registrySent) {
|
|
110
|
+
await buildAndSendSfcRegistry(server, sfcFileMap, wss);
|
|
111
|
+
registrySent = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.warn("[pure-vue-ws] Failed to send SFC registry:", e?.message || e);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// console.log("[pure-vue-ws] WebSocket server configured on /ns-hmr");
|
|
119
|
+
},
|
|
120
|
+
async handleHotUpdate(ctx) {
|
|
121
|
+
const { file, server } = ctx;
|
|
122
|
+
if (!wss)
|
|
123
|
+
return;
|
|
124
|
+
// Fast-path: handle plain CSS updates via our single socket with a dedicated message
|
|
125
|
+
if (file.endsWith(".css")) {
|
|
126
|
+
try {
|
|
127
|
+
const root = server.config.root || process.cwd();
|
|
128
|
+
let rel = path.posix.join("/", path.posix
|
|
129
|
+
.normalize(path.relative(root, file))
|
|
130
|
+
.split(path.sep)
|
|
131
|
+
.join("/"));
|
|
132
|
+
if (!rel.startsWith("/"))
|
|
133
|
+
rel = "/" + rel;
|
|
134
|
+
const ts = Date.now();
|
|
135
|
+
const isHttps = !!server.config.server?.https;
|
|
136
|
+
let origin = null;
|
|
137
|
+
const urls = server.resolvedUrls;
|
|
138
|
+
if (urls?.local?.length) {
|
|
139
|
+
origin = String(urls.local[0]).replace(/\/$/, "");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const httpServer = server.httpServer;
|
|
143
|
+
const addr = httpServer?.address?.();
|
|
144
|
+
const port = Number(server.config.server?.port || addr?.port || 5173);
|
|
145
|
+
const hostCfg = server.config.server?.host;
|
|
146
|
+
const host = typeof hostCfg === "string" && hostCfg !== "0.0.0.0" ? hostCfg : "127.0.0.1";
|
|
147
|
+
origin = `${isHttps ? "https" : "http"}://${host}:${port}`;
|
|
148
|
+
}
|
|
149
|
+
const update = { type: "css-update", path: rel, acceptedPath: rel, timestamp: ts };
|
|
150
|
+
const cssMsg = { type: "ns:css-updates", origin: origin || undefined, updates: [update] };
|
|
151
|
+
wss.clients.forEach((client) => {
|
|
152
|
+
if (client.readyState === client.OPEN)
|
|
153
|
+
client.send(JSON.stringify(cssMsg));
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
console.warn("[pure-vue-ws] Failed to process CSS HMR for", file, e?.message || e);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!file.endsWith(".vue"))
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
// Build a project-root relative path that matches Vite dev served path
|
|
166
|
+
const root = server.config.root || process.cwd();
|
|
167
|
+
let rel = path.posix.join("/", path.posix
|
|
168
|
+
.normalize(path.relative(root, file))
|
|
169
|
+
.split(path.sep)
|
|
170
|
+
.join("/"));
|
|
171
|
+
// Ensure leading slash
|
|
172
|
+
if (!rel.startsWith("/"))
|
|
173
|
+
rel = "/" + rel;
|
|
174
|
+
// Construct template path the client can request from the dev server
|
|
175
|
+
const templatePath = `${rel}?vue&type=template`;
|
|
176
|
+
const id = path
|
|
177
|
+
.basename(file)
|
|
178
|
+
.replace(/\.vue$/i, "")
|
|
179
|
+
.toLowerCase();
|
|
180
|
+
// Compute a stable hashed HMR id similar to webpack/vue plugin behavior
|
|
181
|
+
const hmrId = createHash("md5").update(rel).digest("hex").slice(0, 8);
|
|
182
|
+
// Get absolute file path for reference (simulator can't access but useful for debugging)
|
|
183
|
+
const absolutePath = `file://${path.resolve(file)}`;
|
|
184
|
+
const ts = Date.now(); // Compute absolute origin for the dev server
|
|
185
|
+
const isHttps = !!server.config.server?.https;
|
|
186
|
+
// Prefer resolvedUrls from Vite when available
|
|
187
|
+
let origin = null;
|
|
188
|
+
const urls = server.resolvedUrls;
|
|
189
|
+
if (urls?.local?.length) {
|
|
190
|
+
// A local URL is like http://127.0.0.1:5173/
|
|
191
|
+
origin = String(urls.local[0]).replace(/\/$/, "");
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const httpServer = server.httpServer;
|
|
195
|
+
const addr = httpServer?.address?.();
|
|
196
|
+
const port = Number(server.config.server?.port || addr?.port || 5173);
|
|
197
|
+
const hostCfg = server.config.server?.host;
|
|
198
|
+
// Default to 127.0.0.1 for iOS simulator reliability
|
|
199
|
+
const host = typeof hostCfg === "string" && hostCfg !== "0.0.0.0"
|
|
200
|
+
? hostCfg
|
|
201
|
+
: "127.0.0.1";
|
|
202
|
+
origin = `${isHttps ? "https" : "http"}://${host}:${port}`;
|
|
203
|
+
}
|
|
204
|
+
// Try to get transformed code to avoid HTTP fetch when possible
|
|
205
|
+
let templateCode;
|
|
206
|
+
try {
|
|
207
|
+
const transformed = await server.transformRequest(templatePath);
|
|
208
|
+
if (transformed?.code)
|
|
209
|
+
templateCode = transformed.code;
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
// non-fatal
|
|
213
|
+
}
|
|
214
|
+
// Helper to embed minimal HMR runtime polyfills and rewrite imports to on-device vendor.mjs
|
|
215
|
+
// Also rewrites any `.vue` sub-imports to local hashed SFC module filenames (./sfc-<hash>.mjs)
|
|
216
|
+
function embedHmrRuntimeInModule(code, importerRel) {
|
|
217
|
+
const vendorMjs = "~/vendor.mjs"; // NativeScript app-root alias
|
|
218
|
+
const hmrPrelude = `
|
|
219
|
+
// Embedded HMR Runtime for NativeScript Simulator (minimal)
|
|
220
|
+
const createHotContext = (id) => ({
|
|
221
|
+
on: (event, handler) => {
|
|
222
|
+
if (!globalThis.__NS_HMR_HANDLERS__) globalThis.__NS_HMR_HANDLERS__ = new Map();
|
|
223
|
+
if (!globalThis.__NS_HMR_HANDLERS__.has(id)) globalThis.__NS_HMR_HANDLERS__.set(id, []);
|
|
224
|
+
globalThis.__NS_HMR_HANDLERS__.get(id).push({ event, handler });
|
|
225
|
+
},
|
|
226
|
+
accept: (handler) => {
|
|
227
|
+
if (!globalThis.__NS_HMR_ACCEPTS__) globalThis.__NS_HMR_ACCEPTS__ = new Map();
|
|
228
|
+
globalThis.__NS_HMR_ACCEPTS__.set(id, handler);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (typeof import.meta === 'undefined') {
|
|
233
|
+
// Basic meta polyfill; hot is not used directly after rewrite
|
|
234
|
+
// @ts-ignore
|
|
235
|
+
globalThis.importMeta = { hot: null };
|
|
236
|
+
} else if (!import.meta.hot) {
|
|
237
|
+
// @ts-ignore
|
|
238
|
+
import.meta.hot = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Expose Vite-like hot context factory
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
243
|
+
const __vite__createHotContext = createHotContext;
|
|
244
|
+
|
|
245
|
+
// Ensure __VUE_HMR_RUNTIME__ exists (client installs a real one)
|
|
246
|
+
if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
|
|
247
|
+
// @ts-ignore
|
|
248
|
+
globalThis.__VUE_HMR_RUNTIME__ = {
|
|
249
|
+
createRecord: () => true,
|
|
250
|
+
reload: () => {},
|
|
251
|
+
rerender: () => {},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Local copy of _export_sfc helper used by Vue SFCs
|
|
256
|
+
function _export_sfc(sfc, props) {
|
|
257
|
+
if (props && Array.isArray(props)) {
|
|
258
|
+
for (const [key, value] of props) {
|
|
259
|
+
if (key === 'render') sfc.render = value;
|
|
260
|
+
else sfc[key] = value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return sfc;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Ensure _defineComponent is available by binding from globalThis (set by the HMR client)
|
|
267
|
+
// Fallback to identity to keep runtime working even if not provided
|
|
268
|
+
const _defineComponent = globalThis._defineComponent || globalThis.defineComponent || ((o) => o);
|
|
269
|
+
`;
|
|
270
|
+
let enhanced = String(code);
|
|
271
|
+
// Remove any @vite/client imports
|
|
272
|
+
enhanced = enhanced.replace(/^.*import.*@vite\/client.*$/gm, "// Removed @vite/client import for simulator compatibility");
|
|
273
|
+
// Remove import.meta.hot assignments and handlers (we supply our own minimal polyfill)
|
|
274
|
+
enhanced = enhanced.replace(/import\.meta\.hot\s*=\s*__vite__createHotContext\([^)]*\);?/g, "// Removed import.meta.hot assignment - using embedded polyfill");
|
|
275
|
+
// Remove entire if (import.meta.hot) { ... } blocks to avoid trailing braces (balanced)
|
|
276
|
+
enhanced = stripImportMetaHotBlocks(enhanced);
|
|
277
|
+
// Remove direct handler registrations like: import.meta.hot.on(..., () => { ... }) and accept((mod) => {...})
|
|
278
|
+
// Robustly strip the entire call including nested arrow function bodies until the matching ');'
|
|
279
|
+
enhanced = enhanced.replace(/import\.meta\.hot\.(?:on|accept)\s*\([\s\S]*?\);\s*/g, "");
|
|
280
|
+
// Remove common Vue HMR runtime statements and orphan else tails
|
|
281
|
+
enhanced = stripVueHmrRuntimeNoise(enhanced);
|
|
282
|
+
// Aggressively strip compiler HMR tail between __hmrId and export default
|
|
283
|
+
enhanced = stripVueHmrTail(enhanced);
|
|
284
|
+
// Drop virtual sub-imports (template/style blocks)
|
|
285
|
+
enhanced = enhanced.replace(/import\s+[^"']*\s*from\s*["'][^"']*[?&]vue&type=[^"']*["'];?/g, "// Removed virtual vue sub-import for simulator compatibility");
|
|
286
|
+
enhanced = enhanced.replace(/import\s*["'][^"']*[?&]vue&type=style[^"']*["'];?/g, "// Removed CSS virtual import");
|
|
287
|
+
// Use the already-loaded on-device vendor bundle for framework imports
|
|
288
|
+
enhanced = enhanced.replace(/from\s*["']@vue\/runtime-core["']/g, `from "${vendorMjs}"`);
|
|
289
|
+
enhanced = enhanced.replace(/from\s*["']vue["']/g, `from "${vendorMjs}"`);
|
|
290
|
+
enhanced = enhanced.replace(/from\s*["']nativescript-vue["']/g, `from "${vendorMjs}"`);
|
|
291
|
+
// Also handle Vite-deps resolved paths just in case
|
|
292
|
+
enhanced = enhanced.replace(/from\s*["']\/node_modules\/\.vite\/deps\/nativescript-vue.*?["']/g, `from "${vendorMjs}"`);
|
|
293
|
+
// Remove export helper import (we provide _export_sfc above)
|
|
294
|
+
enhanced = enhanced.replace(/^.*import\s+\{\s*?_export_sfc\s*?\}.*$/gm, "// Removed _export_sfc import - using embedded helper");
|
|
295
|
+
// Remove remaining virtual id imports
|
|
296
|
+
enhanced = enhanced.replace(/^.*import.*\/@id\/__x00__.*$/gm, "// Removed virtual import id");
|
|
297
|
+
// Rewrite .vue sub-component imports to device-local hashed SFC files (strip query if present)
|
|
298
|
+
enhanced = enhanced.replace(/(import\s+[^;]*?from\s*["'])([^"'?]*\.vue)(?:\?[^"']*)?(["'])/g, (_m, p1, spec, p3) => {
|
|
299
|
+
const importerDir = path.posix.dirname(importerRel);
|
|
300
|
+
let key = spec;
|
|
301
|
+
if (!key.startsWith("/")) {
|
|
302
|
+
// Resolve relative spec to project-absolute (leading slash)
|
|
303
|
+
key = path.posix.normalize(path.posix.join(importerDir, spec));
|
|
304
|
+
if (!key.startsWith("/"))
|
|
305
|
+
key = "/" + key;
|
|
306
|
+
}
|
|
307
|
+
const fileName = sfcFileMap.get(key);
|
|
308
|
+
if (!fileName)
|
|
309
|
+
return `${p1}${spec}${p3}`;
|
|
310
|
+
return `${p1}./${fileName}${p3}`;
|
|
311
|
+
});
|
|
312
|
+
// Remove imports of core runtime helpers (we'll map to globals) and inject underscore aliases
|
|
313
|
+
const coreFns = [
|
|
314
|
+
"resolveComponent",
|
|
315
|
+
"createVNode",
|
|
316
|
+
"createTextVNode",
|
|
317
|
+
"withCtx",
|
|
318
|
+
"openBlock",
|
|
319
|
+
"createBlock",
|
|
320
|
+
"createElementVNode",
|
|
321
|
+
"createElementBlock",
|
|
322
|
+
];
|
|
323
|
+
enhanced = removeNamedImports(enhanced, coreFns);
|
|
324
|
+
const usesUnderscoreHelpers = /\b_(resolveComponent|createVNode|createTextVNode|withCtx|openBlock|createBlock|createElementVNode|createElementBlock)\b/.test(enhanced);
|
|
325
|
+
if (usesUnderscoreHelpers) {
|
|
326
|
+
const globalsPrelude = [
|
|
327
|
+
"const _resolveComponent = globalThis._resolveComponent || globalThis.resolveComponent;",
|
|
328
|
+
"const _createVNode = globalThis._createVNode || globalThis.createVNode;",
|
|
329
|
+
"const _createTextVNode = globalThis._createTextVNode || globalThis.createTextVNode;",
|
|
330
|
+
"const _withCtx = globalThis._withCtx || globalThis.withCtx;",
|
|
331
|
+
"const _openBlock = globalThis._openBlock || globalThis.openBlock;",
|
|
332
|
+
"const _createBlock = globalThis._createBlock || globalThis.createBlock;",
|
|
333
|
+
"const _createElementVNode = globalThis._createElementVNode || globalThis.createElementVNode || globalThis.createVNode;",
|
|
334
|
+
"const _createElementBlock = globalThis._createElementBlock || globalThis.createElementBlock || globalThis.createBlock;",
|
|
335
|
+
].join("\n");
|
|
336
|
+
enhanced = globalsPrelude + "\n" + enhanced;
|
|
337
|
+
}
|
|
338
|
+
// Remove lingering imports of reactive helpers and bind from globals if used
|
|
339
|
+
const reactiveFns = ["ref", "computed", "onMounted", "onUnmounted"];
|
|
340
|
+
enhanced = removeNamedImports(enhanced, reactiveFns);
|
|
341
|
+
// Remove any residual import lines for these names
|
|
342
|
+
enhanced = enhanced.replace(/^.*import\s*\{[^}]*\b(ref|computed|onMounted|onUnmounted)\b[^}]*\}\s*from\s*["'][^"']+["'];?\s*$/gm, "");
|
|
343
|
+
const reactiveToBind = [];
|
|
344
|
+
if (/\bref\b/.test(enhanced))
|
|
345
|
+
reactiveToBind.push("ref");
|
|
346
|
+
if (/\bcomputed\b/.test(enhanced))
|
|
347
|
+
reactiveToBind.push("computed");
|
|
348
|
+
if (/\bonMounted\b/.test(enhanced))
|
|
349
|
+
reactiveToBind.push("onMounted");
|
|
350
|
+
if (/\bonUnmounted\b/.test(enhanced))
|
|
351
|
+
reactiveToBind.push("onUnmounted");
|
|
352
|
+
if (reactiveToBind.length) {
|
|
353
|
+
enhanced = injectGlobalsBindings(enhanced, reactiveToBind);
|
|
354
|
+
}
|
|
355
|
+
// If code uses $navigate helpers, import only missing ones from vendor to avoid duplicates
|
|
356
|
+
const usesNavTo = /\$navigateTo\b/.test(enhanced);
|
|
357
|
+
const usesNavBack = /\$navigateBack\b/.test(enhanced);
|
|
358
|
+
// Remove any named imports of these helpers from any source to avoid missing exports
|
|
359
|
+
if (usesNavTo || usesNavBack) {
|
|
360
|
+
const names = [];
|
|
361
|
+
if (usesNavTo)
|
|
362
|
+
names.push("$navigateTo");
|
|
363
|
+
if (usesNavBack)
|
|
364
|
+
names.push("$navigateBack");
|
|
365
|
+
enhanced = removeNamedImports(enhanced, names);
|
|
366
|
+
// Inject globals-based bindings
|
|
367
|
+
enhanced = injectGlobalsBindings(enhanced, names);
|
|
368
|
+
}
|
|
369
|
+
// Finally, prepend the minimal HMR prelude
|
|
370
|
+
return hmrPrelude + "\n" + enhanced;
|
|
371
|
+
}
|
|
372
|
+
const msg = {
|
|
373
|
+
type: "ns:vue-template-url",
|
|
374
|
+
id,
|
|
375
|
+
hmrId,
|
|
376
|
+
path: rel,
|
|
377
|
+
absolutePath,
|
|
378
|
+
templatePath,
|
|
379
|
+
templateUrl: origin ? `${origin}${templatePath}` : undefined,
|
|
380
|
+
origin: origin || undefined,
|
|
381
|
+
code: templateCode,
|
|
382
|
+
ts,
|
|
383
|
+
};
|
|
384
|
+
// APPROACH: Generate new .mjs module for dynamic import (preserves context, uses proper module system)
|
|
385
|
+
try {
|
|
386
|
+
const source = readFileSync(file, "utf8");
|
|
387
|
+
// Transform the entire Vue SFC to a module via Vite's pipeline
|
|
388
|
+
const transformResult = await server.transformRequest(rel);
|
|
389
|
+
if (transformResult?.code) {
|
|
390
|
+
// Before sending dynamic module, ensure any newly imported .vue deps are registered
|
|
391
|
+
try {
|
|
392
|
+
const importerDir = path.posix.dirname(rel);
|
|
393
|
+
const depSpecs = [];
|
|
394
|
+
const depRe = /(import\s+[^;]*?from\s*["'])([^"'?]*\.vue)(?:\?[^"']*)?["']/g;
|
|
395
|
+
let mm;
|
|
396
|
+
while ((mm = depRe.exec(transformResult.code)) !== null) {
|
|
397
|
+
const spec = mm[2];
|
|
398
|
+
let key = spec;
|
|
399
|
+
if (!key.startsWith("/")) {
|
|
400
|
+
key = path.posix.normalize(path.posix.join(importerDir, spec));
|
|
401
|
+
if (!key.startsWith("/"))
|
|
402
|
+
key = "/" + key;
|
|
403
|
+
}
|
|
404
|
+
depSpecs.push(key);
|
|
405
|
+
}
|
|
406
|
+
for (const depRel of depSpecs) {
|
|
407
|
+
if (!sfcFileMap.has(depRel)) {
|
|
408
|
+
const depResult = await server.transformRequest(depRel);
|
|
409
|
+
if (depResult?.code) {
|
|
410
|
+
const sfcHash = createHash("md5").update(depRel).digest("hex").slice(0, 8);
|
|
411
|
+
const depFileName = `sfc-${sfcHash}.mjs`;
|
|
412
|
+
sfcFileMap.set(depRel, depFileName);
|
|
413
|
+
// Clean and rewrite similar to registry path
|
|
414
|
+
let depCode = depResult.code
|
|
415
|
+
.replace(/^.*import.*@vite\/client.*$/gm, "")
|
|
416
|
+
.replace(/import\.meta\.hot\s*=\s*__vite__createHotContext\([^)]*\);?/g, "");
|
|
417
|
+
depCode = stripImportMetaHotBlocks(depCode);
|
|
418
|
+
depCode = depCode.replace(/import\.meta\.hot\.(?:on|accept)\s*\([\s\S]*?\);\s*/g, "");
|
|
419
|
+
depCode = depCode
|
|
420
|
+
.replace(/import\s+[^"']*\s*from\s*["'][^"']*[?&]vue&type=[^"']*["'];?/g, "")
|
|
421
|
+
.replace(/import\s*["'][^"']*[?&]vue&type=style[^"']*["'];?/g, "")
|
|
422
|
+
.replace(/from\s*["']@vue\/runtime-core["']/g, 'from "~/vendor.mjs"')
|
|
423
|
+
.replace(/from\s*["']vue["']/g, 'from "~/vendor.mjs"')
|
|
424
|
+
.replace(/from\s*["']nativescript-vue["']/g, 'from "~/vendor.mjs"')
|
|
425
|
+
.replace(/from\s*["']\/node_modules\/\.vite\/deps\/nativescript-vue.*?["']/g, 'from "~/vendor.mjs"')
|
|
426
|
+
.replace(/^.*import\s+\{\s*?_export_sfc\s*?\}.*$/gm, "")
|
|
427
|
+
.replace(/^.*import.*\/@id\/__x00__.*$/gm, "");
|
|
428
|
+
depCode = stripVueHmrRuntimeNoise(depCode);
|
|
429
|
+
depCode = stripVueHmrTail(depCode);
|
|
430
|
+
// Export helper and _defineComponent binding
|
|
431
|
+
const exportHelperPrelude = `function _export_sfc(sfc, props){if(props&&Array.isArray(props)){for(const [k,v] of props){if(k==='render')sfc.render=v;else sfc[k]=v;}}return sfc;}`;
|
|
432
|
+
if (/\b_defineComponent\b/.test(depCode) && !/\bconst\s+_defineComponent\b/.test(depCode)) {
|
|
433
|
+
depCode = `const _defineComponent = globalThis._defineComponent || globalThis.defineComponent || ((o) => o);\n` + depCode;
|
|
434
|
+
}
|
|
435
|
+
// Remove helper imports and inject globals like registry path
|
|
436
|
+
const coreFns = [
|
|
437
|
+
"resolveComponent",
|
|
438
|
+
"createVNode",
|
|
439
|
+
"createTextVNode",
|
|
440
|
+
"withCtx",
|
|
441
|
+
"openBlock",
|
|
442
|
+
"createBlock",
|
|
443
|
+
"createElementVNode",
|
|
444
|
+
"createElementBlock",
|
|
445
|
+
];
|
|
446
|
+
depCode = removeNamedImports(depCode, coreFns);
|
|
447
|
+
if (/\b_(resolveComponent|createVNode|createTextVNode|withCtx|openBlock|createBlock|createElementVNode|createElementBlock)\b/.test(depCode)) {
|
|
448
|
+
const globalsPrelude = [
|
|
449
|
+
"const _resolveComponent = globalThis._resolveComponent || globalThis.resolveComponent;",
|
|
450
|
+
"const _createVNode = globalThis._createVNode || globalThis.createVNode;",
|
|
451
|
+
"const _createTextVNode = globalThis._createTextVNode || globalThis.createTextVNode;",
|
|
452
|
+
"const _withCtx = globalThis._withCtx || globalThis.withCtx;",
|
|
453
|
+
"const _openBlock = globalThis._openBlock || globalThis.openBlock;",
|
|
454
|
+
"const _createBlock = globalThis._createBlock || globalThis.createBlock;",
|
|
455
|
+
"const _createElementVNode = globalThis._createElementVNode || globalThis.createElementVNode || globalThis.createVNode;",
|
|
456
|
+
"const _createElementBlock = globalThis._createElementBlock || globalThis.createElementBlock || globalThis.createBlock;",
|
|
457
|
+
].join("\n");
|
|
458
|
+
depCode = globalsPrelude + "\n" + depCode;
|
|
459
|
+
}
|
|
460
|
+
const reactiveFns = ["ref", "computed", "onMounted", "onUnmounted"];
|
|
461
|
+
depCode = removeNamedImports(depCode, reactiveFns);
|
|
462
|
+
depCode = depCode.replace(/^.*import\s*\{[^}]*\b(ref|computed|onMounted|onUnmounted)\b[^}]*\}\s*from\s*["'][^"']+["'];?\s*$/gm, "");
|
|
463
|
+
const reactiveToBind = [];
|
|
464
|
+
if (/\bref\b/.test(depCode))
|
|
465
|
+
reactiveToBind.push("ref");
|
|
466
|
+
if (/\bcomputed\b/.test(depCode))
|
|
467
|
+
reactiveToBind.push("computed");
|
|
468
|
+
if (/\bonMounted\b/.test(depCode))
|
|
469
|
+
reactiveToBind.push("onMounted");
|
|
470
|
+
if (/\bonUnmounted\b/.test(depCode))
|
|
471
|
+
reactiveToBind.push("onUnmounted");
|
|
472
|
+
if (reactiveToBind.length)
|
|
473
|
+
depCode = injectGlobalsBindings(depCode, reactiveToBind);
|
|
474
|
+
// Nav helpers
|
|
475
|
+
if (/\$navigate(To|Back)\b/.test(depCode)) {
|
|
476
|
+
depCode = removeNamedImports(depCode, ["$navigateTo", "$navigateBack"]);
|
|
477
|
+
depCode = injectGlobalsBindings(depCode, ["$navigateTo", "$navigateBack"]);
|
|
478
|
+
}
|
|
479
|
+
depCode = exportHelperPrelude + "\n" + depCode;
|
|
480
|
+
const depUpdateMsg = {
|
|
481
|
+
type: "ns:vue-sfc-registry-update",
|
|
482
|
+
path: depRel,
|
|
483
|
+
fileName: depFileName,
|
|
484
|
+
code: depCode,
|
|
485
|
+
ts,
|
|
486
|
+
};
|
|
487
|
+
wss.clients.forEach((client) => {
|
|
488
|
+
if (client.readyState === client.OPEN)
|
|
489
|
+
client.send(JSON.stringify(depUpdateMsg));
|
|
490
|
+
});
|
|
491
|
+
console.log(`[pure-vue-websocket] Registered new SFC dep ${depRel} -> ${depFileName}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
console.warn("[pure-vue-websocket] Failed ensuring SFC deps:", e?.message || e);
|
|
498
|
+
}
|
|
499
|
+
// Generate unique module name for this HMR update
|
|
500
|
+
const moduleId = `hmr-${id}-${ts}`;
|
|
501
|
+
const modulePath = `/${rel}?hmr=${ts}`;
|
|
502
|
+
// Send dynamic module reload instruction with embedded HMR runtime
|
|
503
|
+
const moduleMsg = {
|
|
504
|
+
type: "ns:vue-dynamic-module",
|
|
505
|
+
id,
|
|
506
|
+
hmrId,
|
|
507
|
+
path: rel,
|
|
508
|
+
absolutePath,
|
|
509
|
+
moduleId,
|
|
510
|
+
modulePath,
|
|
511
|
+
moduleCode: embedHmrRuntimeInModule(transformResult.code, rel), // Apply HMR runtime embedding here!
|
|
512
|
+
originalSource: source,
|
|
513
|
+
ts,
|
|
514
|
+
};
|
|
515
|
+
wss.clients.forEach((client) => {
|
|
516
|
+
if (client.readyState === client.OPEN)
|
|
517
|
+
client.send(JSON.stringify(moduleMsg));
|
|
518
|
+
});
|
|
519
|
+
console.log(`[pure-vue-websocket] Sent dynamic module for ${rel}: ${modulePath}`);
|
|
520
|
+
// Additionally, if app-level CSS exists (e.g., /src/app.css), emit CSS update so Tailwind class changes apply
|
|
521
|
+
try {
|
|
522
|
+
const appCssFs = path.resolve(root, "src/app.css");
|
|
523
|
+
let cssExists = false;
|
|
524
|
+
try {
|
|
525
|
+
const st = statSync(appCssFs);
|
|
526
|
+
cssExists = st?.isFile?.() || false;
|
|
527
|
+
}
|
|
528
|
+
catch { }
|
|
529
|
+
if (cssExists) {
|
|
530
|
+
const cssRel = "/src/app.css";
|
|
531
|
+
const update = {
|
|
532
|
+
type: "css-update",
|
|
533
|
+
path: cssRel,
|
|
534
|
+
acceptedPath: cssRel,
|
|
535
|
+
timestamp: ts,
|
|
536
|
+
};
|
|
537
|
+
const cssMsg = {
|
|
538
|
+
type: "ns:css-updates",
|
|
539
|
+
origin: origin || undefined,
|
|
540
|
+
updates: [update],
|
|
541
|
+
};
|
|
542
|
+
wss.clients.forEach((client) => {
|
|
543
|
+
if (client.readyState === client.OPEN)
|
|
544
|
+
client.send(JSON.stringify(cssMsg));
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
console.warn("[pure-vue-websocket] Failed emitting Tailwind CSS update:", e?.message || e);
|
|
550
|
+
}
|
|
551
|
+
// Also set the transformed code for template URL fallback
|
|
552
|
+
templateCode = transformResult.code;
|
|
553
|
+
// Additionally, update (or add) the corresponding registry SFC module on device
|
|
554
|
+
try {
|
|
555
|
+
const sfcHash = createHash("md5")
|
|
556
|
+
.update(rel)
|
|
557
|
+
.digest("hex")
|
|
558
|
+
.slice(0, 8);
|
|
559
|
+
const fileName = sfcFileMap.get(rel) || `sfc-${sfcHash}.mjs`;
|
|
560
|
+
sfcFileMap.set(rel, fileName);
|
|
561
|
+
// Apply similar transforms as in buildAndSendSfcRegistry
|
|
562
|
+
let sfcCode = transformResult.code
|
|
563
|
+
// Remove @vite/client
|
|
564
|
+
.replace(/^.*import.*@vite\/client.*$/gm, "")
|
|
565
|
+
// Remove import.meta.hot assignment
|
|
566
|
+
.replace(/import\.meta\.hot\s*=\s*__vite__createHotContext\([^)]*\);?/g, "");
|
|
567
|
+
sfcCode = stripImportMetaHotBlocks(sfcCode);
|
|
568
|
+
sfcCode = sfcCode.replace(/import\.meta\.hot\.(?:on|accept)\s*\([\s\S]*?\);\s*/g, "");
|
|
569
|
+
// Remove virtual blocks
|
|
570
|
+
sfcCode = sfcCode
|
|
571
|
+
.replace(/import\s+[^"']*\s*from\s*["'][^"']*[?&]vue&type=[^"']*["'];?/g, "")
|
|
572
|
+
.replace(/import\s*["'][^"']*[?&]vue&type=style[^"']*["'];?/g, "")
|
|
573
|
+
// Vendor rewrites
|
|
574
|
+
.replace(/from\s*["']@vue\/runtime-core["']/g, 'from "~/vendor.mjs"')
|
|
575
|
+
.replace(/from\s*["']vue["']/g, 'from "~/vendor.mjs"')
|
|
576
|
+
.replace(/from\s*["']nativescript-vue["']/g, 'from "~/vendor.mjs"')
|
|
577
|
+
.replace(/from\s*["']\/node_modules\/\.vite\/deps\/nativescript-vue.*?["']/g, 'from "~/vendor.mjs"')
|
|
578
|
+
// Remove export helper import (we inject our own)
|
|
579
|
+
.replace(/^.*import\s+\{\s*?_export_sfc\s*?\}.*$/gm, "")
|
|
580
|
+
// Remove remaining virtual id imports
|
|
581
|
+
.replace(/^.*import.*\/@id\/__x00__.*$/gm, "");
|
|
582
|
+
// Remove Vue HMR runtime noise and orphan else
|
|
583
|
+
sfcCode = stripVueHmrRuntimeNoise(sfcCode);
|
|
584
|
+
sfcCode = stripVueHmrTail(sfcCode);
|
|
585
|
+
// Rewrite internal .vue imports to hashed files (strip any query)
|
|
586
|
+
const importerDir = path.posix.dirname(rel);
|
|
587
|
+
sfcCode = sfcCode.replace(/(import\s+[^;]*?from\s*["'])([^"'?]*\.vue)(?:\?[^"']*)?(["'])/g, (_m, p1, spec, p3) => {
|
|
588
|
+
let key = spec;
|
|
589
|
+
if (!key.startsWith("/")) {
|
|
590
|
+
key = path.posix.normalize(path.posix.join(importerDir, spec));
|
|
591
|
+
if (!key.startsWith("/"))
|
|
592
|
+
key = "/" + key;
|
|
593
|
+
}
|
|
594
|
+
const depFile = sfcFileMap.get(key);
|
|
595
|
+
if (!depFile)
|
|
596
|
+
return `${p1}${spec}${p3}`;
|
|
597
|
+
return `${p1}./${depFile}${p3}`;
|
|
598
|
+
});
|
|
599
|
+
const exportHelperPrelude = `function _export_sfc(sfc, props){if(props&&Array.isArray(props)){for(const [k,v] of props){if(k==='render')sfc.render=v;else sfc[k]=v;}}return sfc;}`;
|
|
600
|
+
// Ensure _defineComponent available via globals
|
|
601
|
+
if (/\b_defineComponent\b/.test(sfcCode) &&
|
|
602
|
+
!/\bconst\s+_defineComponent\b/.test(sfcCode)) {
|
|
603
|
+
sfcCode =
|
|
604
|
+
`const _defineComponent = globalThis._defineComponent || globalThis.defineComponent || ((o) => o);\n` +
|
|
605
|
+
sfcCode;
|
|
606
|
+
}
|
|
607
|
+
// Remove core runtime helper imports and map to globals
|
|
608
|
+
const coreFns = [
|
|
609
|
+
"resolveComponent",
|
|
610
|
+
"createVNode",
|
|
611
|
+
"createTextVNode",
|
|
612
|
+
"withCtx",
|
|
613
|
+
"openBlock",
|
|
614
|
+
"createBlock",
|
|
615
|
+
"createElementVNode",
|
|
616
|
+
"createElementBlock",
|
|
617
|
+
];
|
|
618
|
+
sfcCode = removeNamedImports(sfcCode, coreFns);
|
|
619
|
+
if (/\b_(resolveComponent|createVNode|createTextVNode|withCtx|openBlock|createBlock|createElementVNode|createElementBlock)\b/.test(sfcCode)) {
|
|
620
|
+
const globalsPrelude = [
|
|
621
|
+
"const _resolveComponent = globalThis._resolveComponent || globalThis.resolveComponent;",
|
|
622
|
+
"const _createVNode = globalThis._createVNode || globalThis.createVNode;",
|
|
623
|
+
"const _createTextVNode = globalThis._createTextVNode || globalThis.createTextVNode;",
|
|
624
|
+
"const _withCtx = globalThis._withCtx || globalThis.withCtx;",
|
|
625
|
+
"const _openBlock = globalThis._openBlock || globalThis.openBlock;",
|
|
626
|
+
"const _createBlock = globalThis._createBlock || globalThis.createBlock;",
|
|
627
|
+
"const _createElementVNode = globalThis._createElementVNode || globalThis.createElementVNode || globalThis.createVNode;",
|
|
628
|
+
"const _createElementBlock = globalThis._createElementBlock || globalThis.createElementBlock || globalThis.createBlock;",
|
|
629
|
+
].join("\n");
|
|
630
|
+
sfcCode = globalsPrelude + "\n" + sfcCode;
|
|
631
|
+
}
|
|
632
|
+
// Remove lingering reactive helper imports and bind from globals if used
|
|
633
|
+
const reactiveFns = ["ref", "computed", "onMounted", "onUnmounted"];
|
|
634
|
+
sfcCode = removeNamedImports(sfcCode, reactiveFns);
|
|
635
|
+
sfcCode = sfcCode.replace(/^.*import\s*\{[^}]*\b(ref|computed|onMounted|onUnmounted)\b[^}]*\}\s*from\s*["'][^"']+["'];?\s*$/gm, "");
|
|
636
|
+
const reactiveToBind = [];
|
|
637
|
+
if (/\bref\b/.test(sfcCode))
|
|
638
|
+
reactiveToBind.push("ref");
|
|
639
|
+
if (/\bcomputed\b/.test(sfcCode))
|
|
640
|
+
reactiveToBind.push("computed");
|
|
641
|
+
if (/\bonMounted\b/.test(sfcCode))
|
|
642
|
+
reactiveToBind.push("onMounted");
|
|
643
|
+
if (/\bonUnmounted\b/.test(sfcCode))
|
|
644
|
+
reactiveToBind.push("onUnmounted");
|
|
645
|
+
if (reactiveToBind.length) {
|
|
646
|
+
sfcCode = injectGlobalsBindings(sfcCode, reactiveToBind);
|
|
647
|
+
}
|
|
648
|
+
// Handle navigation helpers via globals
|
|
649
|
+
const usesNavTo = /\$navigateTo\b/.test(sfcCode);
|
|
650
|
+
const usesNavBack = /\$navigateBack\b/.test(sfcCode);
|
|
651
|
+
if (usesNavTo || usesNavBack) {
|
|
652
|
+
const names = [];
|
|
653
|
+
if (usesNavTo)
|
|
654
|
+
names.push("$navigateTo");
|
|
655
|
+
if (usesNavBack)
|
|
656
|
+
names.push("$navigateBack");
|
|
657
|
+
sfcCode = removeNamedImports(sfcCode, names);
|
|
658
|
+
sfcCode = injectGlobalsBindings(sfcCode, names);
|
|
659
|
+
}
|
|
660
|
+
sfcCode = exportHelperPrelude + "\n" + sfcCode;
|
|
661
|
+
const updateMsg = {
|
|
662
|
+
type: "ns:vue-sfc-registry-update",
|
|
663
|
+
path: rel,
|
|
664
|
+
fileName,
|
|
665
|
+
code: sfcCode,
|
|
666
|
+
ts,
|
|
667
|
+
};
|
|
668
|
+
wss.clients.forEach((client) => {
|
|
669
|
+
if (client.readyState === client.OPEN)
|
|
670
|
+
client.send(JSON.stringify(updateMsg));
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch (e) {
|
|
674
|
+
console.warn("[pure-vue-websocket] Failed to send SFC registry update for", rel, e);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// FALLBACK: Send raw SFC source for on-device compilation if needed
|
|
678
|
+
const sfcMsg = {
|
|
679
|
+
type: "ns:vue-sfc-source",
|
|
680
|
+
id,
|
|
681
|
+
hmrId,
|
|
682
|
+
path: rel,
|
|
683
|
+
absolutePath,
|
|
684
|
+
source,
|
|
685
|
+
ts,
|
|
686
|
+
};
|
|
687
|
+
wss.clients.forEach((client) => {
|
|
688
|
+
if (client.readyState === client.OPEN)
|
|
689
|
+
client.send(JSON.stringify(sfcMsg));
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
console.warn("[pure-vue-websocket] Failed to generate dynamic module:", e);
|
|
694
|
+
}
|
|
695
|
+
// SECOND: Send transformed template as fallback
|
|
696
|
+
console.log("[pure-vue-ws] template-url (fallback)", {
|
|
697
|
+
path: rel,
|
|
698
|
+
hmrId,
|
|
699
|
+
templateUrl: msg.templateUrl,
|
|
700
|
+
hasCode: !!templateCode,
|
|
701
|
+
});
|
|
702
|
+
wss.clients.forEach((client) => {
|
|
703
|
+
if (client.readyState === client.OPEN)
|
|
704
|
+
client.send(JSON.stringify(msg));
|
|
705
|
+
});
|
|
706
|
+
// Note: keeping template-url as fallback for debugging/alternative paths
|
|
707
|
+
}
|
|
708
|
+
catch (e) {
|
|
709
|
+
console.warn("[pure-vue-ws] Failed to process HMR for", file, e?.message || e);
|
|
710
|
+
}
|
|
711
|
+
return ctx.modules;
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
// Recursively walk directories to find .vue files (excluding common large/irrelevant folders)
|
|
716
|
+
// TODO: improve this to walk from base src directory (need to think about workspaces)
|
|
717
|
+
function walkVueFiles(dir, acc, root) {
|
|
718
|
+
for (const name of readdirSync(dir)) {
|
|
719
|
+
const full = path.join(dir, name);
|
|
720
|
+
const rel = path.relative(root, full);
|
|
721
|
+
// skip
|
|
722
|
+
if (name === "node_modules" ||
|
|
723
|
+
name === ".git" ||
|
|
724
|
+
name === "dist" ||
|
|
725
|
+
name === '.ns-vite-build' ||
|
|
726
|
+
name === "hooks" ||
|
|
727
|
+
name === "platform" ||
|
|
728
|
+
name === "platforms" ||
|
|
729
|
+
name === "App_Resources") {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const st = statSync(full);
|
|
733
|
+
if (st.isDirectory()) {
|
|
734
|
+
walkVueFiles(full, acc, root);
|
|
735
|
+
}
|
|
736
|
+
else if (st.isFile() && name.endsWith(".vue")) {
|
|
737
|
+
acc.push("/" + rel.split(path.sep).join("/"));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function buildAndSendSfcRegistry(server, sfcFileMap, wss) {
|
|
742
|
+
const root = server.config.root || process.cwd();
|
|
743
|
+
const vueFiles = [];
|
|
744
|
+
walkVueFiles(root, vueFiles, root);
|
|
745
|
+
if (!vueFiles.length)
|
|
746
|
+
return;
|
|
747
|
+
// Prepare entries
|
|
748
|
+
const entries = [];
|
|
749
|
+
for (const rel of vueFiles) {
|
|
750
|
+
// Generate stable hashed filename for device-local storage
|
|
751
|
+
const hash = createHash("md5").update(rel).digest("hex").slice(0, 8);
|
|
752
|
+
const fileName = `sfc-${hash}.mjs`;
|
|
753
|
+
sfcFileMap.set(rel, fileName);
|
|
754
|
+
}
|
|
755
|
+
// Transform all and rewrite their own .vue imports to hashed names and vendor path
|
|
756
|
+
for (const rel of vueFiles) {
|
|
757
|
+
try {
|
|
758
|
+
const result = await server.transformRequest(rel);
|
|
759
|
+
if (!result?.code)
|
|
760
|
+
continue;
|
|
761
|
+
let code = result.code;
|
|
762
|
+
// Apply same vendor/virtual/remove-hot rewrites as dynamic modules, but no HMR prelude
|
|
763
|
+
code = code
|
|
764
|
+
// Remove @vite/client
|
|
765
|
+
.replace(/^.*import.*@vite\/client.*$/gm, "// Removed @vite/client import")
|
|
766
|
+
// Remove import.meta.hot assignment and blocks
|
|
767
|
+
.replace(/import\.meta\.hot\s*=\s*__vite__createHotContext\([^)]*\);?/g, "");
|
|
768
|
+
code = stripImportMetaHotBlocks(code);
|
|
769
|
+
code = code.replace(/import\.meta\.hot\.(?:on|accept)\s*\([\s\S]*?\);\s*/g, "");
|
|
770
|
+
code = code
|
|
771
|
+
// Remove virtual blocks
|
|
772
|
+
.replace(/import\s+[^"']*\s*from\s*["'][^"']*[?&]vue&type=[^"']*["'];?/g, "// Removed virtual vue import")
|
|
773
|
+
.replace(/import\s*["'][^"']*[?&]vue&type=style[^"']*["'];?/g, "// Removed CSS virtual import")
|
|
774
|
+
// Vendor rewrites
|
|
775
|
+
.replace(/from\s*["']@vue\/runtime-core["']/g, 'from "~/vendor.mjs"')
|
|
776
|
+
.replace(/from\s*["']vue["']/g, 'from "~/vendor.mjs"')
|
|
777
|
+
.replace(/from\s*["']nativescript-vue["']/g, 'from "~/vendor.mjs"')
|
|
778
|
+
.replace(/from\s*["']\/node_modules\/\.vite\/deps\/nativescript-vue.*?["']/g, 'from "~/vendor.mjs"')
|
|
779
|
+
// Remove export helper import (we provide one in registry output below)
|
|
780
|
+
.replace(/^.*import\s+\{\s*?_export_sfc\s*?\}.*$/gm, "")
|
|
781
|
+
// Remove remaining virtual id imports
|
|
782
|
+
.replace(/^.*import.*\/@id\/__x00__.*$/gm, "");
|
|
783
|
+
code = stripVueHmrRuntimeNoise(code);
|
|
784
|
+
code = stripVueHmrTail(code);
|
|
785
|
+
// Inject a minimal _export_sfc helper at top to satisfy compiled SFC default export pattern
|
|
786
|
+
const exportHelperPrelude = `function _export_sfc(sfc, props){if(props&&Array.isArray(props)){for(const [k,v] of props){if(k==='render')sfc.render=v;else sfc[k]=v;}}return sfc;}`;
|
|
787
|
+
code = exportHelperPrelude + "\n" + code;
|
|
788
|
+
// Rewrite its own .vue imports to hashed SFC local files, resolving relative paths against importer (strip any query)
|
|
789
|
+
const importerDir = path.posix.dirname(rel);
|
|
790
|
+
code = code.replace(/(import\s+[^;]*?from\s*["'])([^"'?]*\.vue)(?:\?[^"']*)?(["'])/g, (_m, p1, importPath, p3) => {
|
|
791
|
+
let key = importPath;
|
|
792
|
+
if (!key.startsWith("/")) {
|
|
793
|
+
key = path.posix.normalize(path.posix.join(importerDir, importPath));
|
|
794
|
+
if (!key.startsWith("/"))
|
|
795
|
+
key = "/" + key;
|
|
796
|
+
}
|
|
797
|
+
const fileName = sfcFileMap.get(key);
|
|
798
|
+
if (!fileName)
|
|
799
|
+
return `${p1}${importPath}${p3}`;
|
|
800
|
+
return `${p1}./${fileName}${p3}`;
|
|
801
|
+
});
|
|
802
|
+
// Ensure _defineComponent is available via globals if referenced
|
|
803
|
+
if (/\b_defineComponent\b/.test(code) &&
|
|
804
|
+
!/\bconst\s+_defineComponent\b/.test(code)) {
|
|
805
|
+
code =
|
|
806
|
+
`const _defineComponent = globalThis._defineComponent || globalThis.defineComponent || ((o) => o);\n` +
|
|
807
|
+
code;
|
|
808
|
+
}
|
|
809
|
+
// Remove imports for core runtime helper functions and map to globals
|
|
810
|
+
const coreFns = [
|
|
811
|
+
"resolveComponent",
|
|
812
|
+
"createVNode",
|
|
813
|
+
"createTextVNode",
|
|
814
|
+
"withCtx",
|
|
815
|
+
"openBlock",
|
|
816
|
+
"createBlock",
|
|
817
|
+
"createElementVNode",
|
|
818
|
+
"createElementBlock",
|
|
819
|
+
];
|
|
820
|
+
code = removeNamedImports(code, coreFns);
|
|
821
|
+
if (/\b_(resolveComponent|createVNode|createTextVNode|withCtx|openBlock|createBlock|createElementVNode|createElementBlock)\b/.test(code)) {
|
|
822
|
+
const globalsPrelude = [
|
|
823
|
+
"const _resolveComponent = globalThis._resolveComponent || globalThis.resolveComponent;",
|
|
824
|
+
"const _createVNode = globalThis._createVNode || globalThis.createVNode;",
|
|
825
|
+
"const _createTextVNode = globalThis._createTextVNode || globalThis.createTextVNode;",
|
|
826
|
+
"const _withCtx = globalThis._withCtx || globalThis.withCtx;",
|
|
827
|
+
"const _openBlock = globalThis._openBlock || globalThis.openBlock;",
|
|
828
|
+
"const _createBlock = globalThis._createBlock || globalThis.createBlock;",
|
|
829
|
+
"const _createElementVNode = globalThis._createElementVNode || globalThis.createElementVNode || globalThis.createVNode;",
|
|
830
|
+
"const _createElementBlock = globalThis._createElementBlock || globalThis.createElementBlock || globalThis.createBlock;",
|
|
831
|
+
].join("\n");
|
|
832
|
+
code = globalsPrelude + "\n" + code;
|
|
833
|
+
}
|
|
834
|
+
// Remove lingering imports of reactive helpers and bind from globals if used
|
|
835
|
+
const reactiveFns = ["ref", "computed", "onMounted", "onUnmounted"];
|
|
836
|
+
code = removeNamedImports(code, reactiveFns);
|
|
837
|
+
code = code.replace(/^.*import\s*\{[^}]*\b(ref|computed|onMounted|onUnmounted)\b[^}]*\}\s*from\s*["'][^"']+["'];?\s*$/gm, "");
|
|
838
|
+
const reactiveToBind = [];
|
|
839
|
+
if (/\bref\b/.test(code))
|
|
840
|
+
reactiveToBind.push("ref");
|
|
841
|
+
if (/\bcomputed\b/.test(code))
|
|
842
|
+
reactiveToBind.push("computed");
|
|
843
|
+
if (/\bonMounted\b/.test(code))
|
|
844
|
+
reactiveToBind.push("onMounted");
|
|
845
|
+
if (/\bonUnmounted\b/.test(code))
|
|
846
|
+
reactiveToBind.push("onUnmounted");
|
|
847
|
+
if (reactiveToBind.length) {
|
|
848
|
+
code = injectGlobalsBindings(code, reactiveToBind);
|
|
849
|
+
}
|
|
850
|
+
// Handle navigation helpers using globals instead of vendor exports
|
|
851
|
+
const usesNavTo = /\$navigateTo\b/.test(code);
|
|
852
|
+
const usesNavBack = /\$navigateBack\b/.test(code);
|
|
853
|
+
if (usesNavTo || usesNavBack) {
|
|
854
|
+
const names = [];
|
|
855
|
+
if (usesNavTo)
|
|
856
|
+
names.push("$navigateTo");
|
|
857
|
+
if (usesNavBack)
|
|
858
|
+
names.push("$navigateBack");
|
|
859
|
+
code = removeNamedImports(code, names);
|
|
860
|
+
code = injectGlobalsBindings(code, names);
|
|
861
|
+
}
|
|
862
|
+
const hmrId = createHash("md5").update(rel).digest("hex").slice(0, 8);
|
|
863
|
+
const fileName = sfcFileMap.get(rel);
|
|
864
|
+
entries.push({ path: rel, fileName, hmrId, code });
|
|
865
|
+
}
|
|
866
|
+
catch (e) {
|
|
867
|
+
console.warn("[pure-vue-ws] Failed transforming", rel, e?.message || e);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (!entries.length)
|
|
871
|
+
return;
|
|
872
|
+
const msg = {
|
|
873
|
+
type: "ns:vue-sfc-registry",
|
|
874
|
+
entries,
|
|
875
|
+
ts: Date.now(),
|
|
876
|
+
};
|
|
877
|
+
wss.clients.forEach((client) => {
|
|
878
|
+
if (client.readyState === client.OPEN)
|
|
879
|
+
client.send(JSON.stringify(msg));
|
|
880
|
+
});
|
|
881
|
+
console.log(`[pure-vue-websocket] Sent SFC registry with ${entries.length} entries`);
|
|
882
|
+
}
|