Skip to content

Commit 15f17df

Browse files
committed
feat: vite support phase 1
Supports live reload. HMR is partial and works hand-in-hand with a hmr-client setup in the per-project @nativescript/vite setup.
1 parent 6de802d commit 15f17df

File tree

2 files changed

+224
-20
lines changed

2 files changed

+224
-20
lines changed

lib/services/bundler/bundler-compiler-service.ts

Lines changed: 220 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ export class BundlerCompilerService
9494
prepareData,
9595
);
9696

97+
// Handle Vite differently from webpack
98+
const isVite = this.getBundler() === "vite";
99+
97100
childProcess.stdout.on("data", function (data) {
98101
process.stdout.write(data);
99102
});
@@ -102,9 +105,124 @@ export class BundlerCompilerService
102105
process.stderr.write(data);
103106
});
104107

108+
// For both Vite and webpack, we wait for the first build to complete
109+
// Don't resolve immediately for Vite - wait for first IPC message
110+
105111
childProcess.on("message", (message: string | IBundlerEmitMessage) => {
106112
this.$logger.trace(`Message from ${projectData.bundler}`, message);
107113

114+
// Handle Vite messages
115+
if (
116+
isVite &&
117+
message &&
118+
(message as IBundlerEmitMessage).emittedFiles
119+
) {
120+
message = message as IBundlerEmitMessage;
121+
console.log("Received Vite IPC message:", message);
122+
123+
// Copy Vite output files directly to platform destination
124+
const distOutput = path.join(projectData.projectDir, "dist");
125+
const destDir = path.join(
126+
platformData.appDestinationDirectoryPath,
127+
this.$options.hostProjectModuleName,
128+
);
129+
130+
console.log(`🔥 Copying from ${distOutput} to ${destDir}`);
131+
132+
// For HMR updates, only copy changed files; for full builds, copy everything
133+
if (
134+
message.isHMR &&
135+
message.changedFiles &&
136+
message.changedFiles.length > 0
137+
) {
138+
console.log(
139+
"🔥 HMR update - copying only changed files for:",
140+
message.changedFiles,
141+
);
142+
143+
// For HTML template changes, we need to copy the component files that were rebuilt
144+
let filesToCopy = message.emittedFiles;
145+
146+
// If we have HTML changes, identify which component files need copying
147+
const hasHTMLChanges = message.changedFiles.some((f) =>
148+
f.endsWith(".html"),
149+
);
150+
if (hasHTMLChanges) {
151+
// Copy component-related files (the ones that would have been rebuilt due to template changes)
152+
filesToCopy = message.emittedFiles.filter(
153+
(f) =>
154+
f.includes(".component") ||
155+
f === "bundle.mjs" ||
156+
f === "bundle.mjs.map",
157+
);
158+
console.log(
159+
"🔥 HTML change detected - copying component files:",
160+
filesToCopy,
161+
);
162+
}
163+
164+
this.copyViteBundleToNative(distOutput, destDir, filesToCopy);
165+
} else {
166+
console.log("🔥 Full build - copying all files");
167+
this.copyViteBundleToNative(distOutput, destDir);
168+
}
169+
170+
// Resolve the promise on first build completion
171+
if (isFirstBundlerWatchCompilation) {
172+
isFirstBundlerWatchCompilation = false;
173+
console.log(
174+
"Vite first build completed, resolving compileWithWatch",
175+
);
176+
resolve(childProcess);
177+
}
178+
179+
// Transform Vite message to match webpack format
180+
const files = (message as IBundlerEmitMessage).emittedFiles.map(
181+
(file) =>
182+
path.join(
183+
platformData.appDestinationDirectoryPath,
184+
this.$options.hostProjectModuleName,
185+
file,
186+
),
187+
);
188+
189+
const data = {
190+
files,
191+
hasOnlyHotUpdateFiles: message.isHMR || false,
192+
hmrData: {
193+
hash: (message as IBundlerEmitMessage).hash || "",
194+
fallbackFiles: [] as string[],
195+
},
196+
platform: platformData.platformNameLowerCase,
197+
};
198+
199+
this.$logger.info(
200+
`Vite build completed! Files copied to native platform.`,
201+
);
202+
// Send HMR notification to connected WebSocket clients first
203+
this.notifyHMRClients({
204+
type: message.isHMR ? "js-update" : "build-complete",
205+
timestamp: Date.now(),
206+
changedFiles: message.changedFiles || [],
207+
buildType: message.buildType || "incremental",
208+
isHMR: message.isHMR || false,
209+
});
210+
211+
if (message.isHMR) {
212+
console.log(
213+
"🔥 Skipping BUNDLER_COMPILATION_COMPLETE for HMR update - app will not restart",
214+
);
215+
} else {
216+
// Only emit BUNDLER_COMPILATION_COMPLETE for non-HMR builds
217+
// This prevents the CLI from restarting the app during HMR updates
218+
console.log(
219+
"🔥 Emitting BUNDLER_COMPILATION_COMPLETE for full build",
220+
);
221+
this.emit(BUNDLER_COMPILATION_COMPLETE, data);
222+
}
223+
return;
224+
}
225+
108226
// if we are on webpack5 - we handle HMR in a slightly different way
109227
if (
110228
typeof message === "object" &&
@@ -228,23 +346,6 @@ export class BundlerCompilerService
228346
this.$logger.trace(
229347
`${capitalizeFirstLetter(projectData.bundler)} process exited with code ${exitCode} when we expected it to be long living with watch.`,
230348
);
231-
if (this.getBundler() === "vite" && exitCode === 0) {
232-
// note experimental: investigate watch mode
233-
const bundlePath = path.join(
234-
projectData.projectDir,
235-
"dist/bundle.js",
236-
);
237-
console.log("bundlePath:", bundlePath);
238-
const data = {
239-
files: [bundlePath],
240-
hasOnlyHotUpdateFiles: false,
241-
hmrData: {},
242-
platform: platformData.platformNameLowerCase,
243-
};
244-
this.emit(BUNDLER_COMPILATION_COMPLETE, data);
245-
resolve(1);
246-
return;
247-
}
248349

249350
await this.$cleanupService.removeKillProcess(
250351
childProcess.pid.toString(),
@@ -367,7 +468,12 @@ export class BundlerCompilerService
367468
);
368469
// Note: With Vite, we need `--` to prevent vite cli from erroring on unknown options.
369470
const envParams = isVite
370-
? [`--mode=${platformData.platformNameLowerCase}`, "--", ...cliArgs]
471+
? [
472+
`--mode=${platformData.platformNameLowerCase}`,
473+
`--watch`,
474+
"--",
475+
...cliArgs,
476+
]
371477
: cliArgs;
372478
const additionalNodeArgs =
373479
semver.major(process.version) <= 8 ? ["--harmony"] : [];
@@ -383,7 +489,7 @@ export class BundlerCompilerService
383489
const args = [
384490
...additionalNodeArgs,
385491
this.getBundlerExecutablePath(projectData),
386-
this.isModernBundler(projectData) ? `build` : null,
492+
isVite ? "build" : this.isModernBundler(projectData) ? `build` : null,
387493
`--config=${projectData.bundlerConfigPath}`,
388494
...envParams,
389495
].filter(Boolean);
@@ -394,7 +500,7 @@ export class BundlerCompilerService
394500
}
395501
}
396502

397-
const stdio = prepareData.watch ? ["ipc"] : "inherit";
503+
const stdio = prepareData.watch || isVite ? ["ipc"] : "inherit";
398504
const options: { [key: string]: any } = {
399505
cwd: projectData.projectDir,
400506
stdio,
@@ -753,6 +859,100 @@ export class BundlerCompilerService
753859
public getBundler(): BundlerType {
754860
return this.$projectConfigService.getValue(`bundler`, "webpack");
755861
}
862+
863+
private copyViteBundleToNative(
864+
distOutput: string,
865+
destDir: string,
866+
specificFiles: string[] = null,
867+
) {
868+
// Clean and copy Vite output to native platform folder
869+
console.log(`Copying Vite bundle from "${distOutput}" to "${destDir}"`);
870+
871+
const fs = require("fs");
872+
873+
try {
874+
if (specificFiles) {
875+
// HMR mode: only copy specific files
876+
console.log("🔥 HMR: Copying specific files:", specificFiles);
877+
878+
// Ensure destination directory exists
879+
fs.mkdirSync(destDir, { recursive: true });
880+
881+
// Copy only the specified files
882+
for (const file of specificFiles) {
883+
const srcPath = path.join(distOutput, file);
884+
const destPath = path.join(destDir, file);
885+
886+
if (!fs.existsSync(srcPath)) continue;
887+
888+
// create parent dirs
889+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
890+
891+
fs.copyFileSync(srcPath, destPath);
892+
893+
console.log(`🔥 HMR: Copied ${file}`);
894+
}
895+
} else {
896+
// Full build mode: clean and copy everything
897+
console.log("🔥 Full build: Copying all files");
898+
899+
// Clean destination directory
900+
if (fs.existsSync(destDir)) {
901+
fs.rmSync(destDir, { recursive: true, force: true });
902+
}
903+
fs.mkdirSync(destDir, { recursive: true });
904+
905+
// Copy all files from dist to platform destination
906+
if (fs.existsSync(distOutput)) {
907+
this.copyRecursiveSync(distOutput, destDir, fs);
908+
} else {
909+
this.$logger.warn(
910+
`Vite output directory does not exist: ${distOutput}`,
911+
);
912+
}
913+
}
914+
} catch (error) {
915+
this.$logger.warn(`Failed to copy Vite bundle: ${error.message}`);
916+
}
917+
}
918+
919+
private notifyHMRClients(message: any) {
920+
// Send WebSocket notification to HMR clients
921+
try {
922+
const WebSocket = require("ws");
923+
924+
// Try to connect to HMR bridge and send notification
925+
const ws = new WebSocket("ws://localhost:24678");
926+
927+
ws.on("open", () => {
928+
console.log("🔥 Sending HMR notification to bridge:", message.type);
929+
ws.send(JSON.stringify(message));
930+
ws.close();
931+
});
932+
933+
ws.on("error", () => {
934+
// HMR bridge not available, which is fine
935+
console.log("🔥 HMR bridge not available (this is normal without HMR)");
936+
});
937+
} catch (error) {
938+
// WebSocket not available, which is fine
939+
console.log("🔥 WebSocket not available for HMR notifications");
940+
}
941+
}
942+
943+
private copyRecursiveSync(src: string, dest: string, fs: any) {
944+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
945+
const srcPath = path.join(src, entry.name);
946+
const destPath = path.join(dest, entry.name);
947+
948+
if (entry.isDirectory()) {
949+
fs.mkdirSync(destPath, { recursive: true });
950+
this.copyRecursiveSync(srcPath, destPath, fs);
951+
} else if (entry.isFile() || entry.isSymbolicLink()) {
952+
fs.copyFileSync(srcPath, destPath);
953+
}
954+
}
955+
}
756956
}
757957

758958
function capitalizeFirstLetter(val: string) {

lib/services/bundler/bundler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ declare global {
7272
emittedFiles: string[];
7373
chunkFiles: string[];
7474
hash: string;
75+
changedFiles?: string[];
76+
isHMR?: boolean;
77+
filesToCopy?: string[];
78+
buildType?: string;
7579
}
7680

7781
interface IPlatformProjectService

0 commit comments

Comments
 (0)