create-vidra-app 0.2.0 → 0.2.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/cli.js +415 -67
- package/dist/index.js +15 -18
- package/package.json +1 -1
- package/templates/react-vite/README.md +16 -1
- package/templates/react-vite/src/{{projectName}}.Host/MainPage.cs +14 -9
- package/templates/react-vite/ui/src/App.tsx +24 -0
- package/templates/react-vite/ui/src/index.css +16 -0
package/dist/cli.js
CHANGED
|
@@ -368,6 +368,7 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
368
368
|
import prompts from "prompts";
|
|
369
369
|
var DOTNET = process.platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
370
370
|
var MAUI_DOCS = "https://learn.microsoft.com/dotnet/maui/get-started/installation";
|
|
371
|
+
var INSTALL_NET_10_FIX = "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download";
|
|
371
372
|
var bufToStr = (v) => v == null ? "" : Buffer.isBuffer(v) ? v.toString() : v;
|
|
372
373
|
var run = (cmd, args) => {
|
|
373
374
|
try {
|
|
@@ -386,107 +387,179 @@ var run = (cmd, args) => {
|
|
|
386
387
|
};
|
|
387
388
|
}
|
|
388
389
|
};
|
|
389
|
-
var
|
|
390
|
-
var
|
|
390
|
+
var splitLines = (text) => text.split(/\r?\n/);
|
|
391
|
+
var matchesAny = (text, patterns) => patterns.some((pattern) => pattern.test(text));
|
|
392
|
+
var NET_10_VERSION = /^10\./;
|
|
393
|
+
var WORKLOAD_SET_VERSION_LINE = /Workload version:\s*([\w.-]+)/i;
|
|
394
|
+
var MAUI_WORKLOAD = /\bmaui\b/i;
|
|
395
|
+
var COMMAND_LINE_TOOLS_PATH = /CommandLineTools/i;
|
|
396
|
+
var hasNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).some((line) => NET_10_VERSION.test(line.trim()));
|
|
397
|
+
var newestNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).map((line) => line.trim().split(/\s+/)[0]).filter((version) => NET_10_VERSION.test(version)).sort(
|
|
391
398
|
(a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" })
|
|
392
399
|
).pop();
|
|
393
|
-
var outputMentionsMaui = (workloadListOutput) =>
|
|
394
|
-
var
|
|
400
|
+
var outputMentionsMaui = (workloadListOutput) => MAUI_WORKLOAD.test(workloadListOutput);
|
|
401
|
+
var workloadSetVersion = (workloadListOutput) => workloadListOutput.match(WORKLOAD_SET_VERSION_LINE)?.[1];
|
|
402
|
+
var MIN_MACCATALYST_WATCH_WORKLOAD_SET = [10, 0, 203];
|
|
403
|
+
var MIN_MACCATALYST_WATCH_SDK = [10, 0, 300];
|
|
404
|
+
var meetsMinimumVersion = (version, minimum) => {
|
|
405
|
+
const segments = version.split(".").map((s) => Number.parseInt(s, 10));
|
|
406
|
+
if (segments.slice(0, minimum.length).some(Number.isNaN)) return false;
|
|
407
|
+
for (let i = 0; i < minimum.length; i++) {
|
|
408
|
+
const segment = segments[i] ?? 0;
|
|
409
|
+
if (segment !== minimum[i]) return segment > minimum[i];
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
};
|
|
413
|
+
var workloadSetSupportsCSharpHotReload = (version) => meetsMinimumVersion(version, MIN_MACCATALYST_WATCH_WORKLOAD_SET);
|
|
414
|
+
var sdkSupportsCSharpHotReload = (version) => meetsMinimumVersion(version, MIN_MACCATALYST_WATCH_SDK);
|
|
415
|
+
var macCatalystHotReloadBlocker = () => {
|
|
416
|
+
const sdkRes = run(DOTNET, ["--version"]);
|
|
417
|
+
if (!sdkRes.found) return null;
|
|
418
|
+
const sdkVersion = sdkRes.stdout.trim();
|
|
419
|
+
if (sdkVersion && !sdkSupportsCSharpHotReload(sdkVersion)) {
|
|
420
|
+
return {
|
|
421
|
+
reason: `C# hot reload needs the .NET SDK 10.0.300+ \u2014 found ${sdkVersion}`,
|
|
422
|
+
fix: INSTALL_NET_10_FIX
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const res = run(DOTNET, ["workload", "list"]);
|
|
426
|
+
if (!res.found) return null;
|
|
427
|
+
const version = workloadSetVersion(res.stdout);
|
|
428
|
+
if (!version || workloadSetSupportsCSharpHotReload(version)) return null;
|
|
429
|
+
return {
|
|
430
|
+
reason: `C# hot reload needs workload set 10.0.203+ \u2014 found ${version}`,
|
|
431
|
+
fix: "dotnet workload update"
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
var MISSING_WORKLOAD_SIGNATURES = [
|
|
395
435
|
/NETSDK1147/i,
|
|
396
436
|
/workloads?\s+must\s+be\s+installed/i,
|
|
397
437
|
/maui-maccatalyst/i,
|
|
398
438
|
/maui-windows/i,
|
|
399
439
|
/to\s+install\s+the\s+.*workload/i
|
|
400
|
-
]
|
|
401
|
-
var
|
|
440
|
+
];
|
|
441
|
+
var MISSING_XCODE_SIGNATURES = [
|
|
402
442
|
/valid\s+Xcode\s+installation\s+was\s+not\s+found/i,
|
|
403
443
|
/could\s+not\s+find\s+a\s+valid\s+Xcode\s+app\s+bundle/i,
|
|
404
444
|
/macios-missing-xcode/i
|
|
405
|
-
]
|
|
445
|
+
];
|
|
446
|
+
var OUTDATED_XCODE_SIGNATURES = [
|
|
447
|
+
/error\s+MT0180/i,
|
|
448
|
+
/requires\s+the\s+MacCatalyst\s+\S+\s+SDK\s+\(shipped\s+with\s+Xcode/i
|
|
449
|
+
];
|
|
450
|
+
var looksLikeMissingWorkload = (output) => matchesAny(output, MISSING_WORKLOAD_SIGNATURES);
|
|
451
|
+
var looksLikeMissingXcode = (output) => matchesAny(output, MISSING_XCODE_SIGNATURES);
|
|
452
|
+
var looksLikeXcodeTooOld = (output) => matchesAny(output, OUTDATED_XCODE_SIGNATURES);
|
|
406
453
|
var checkDotnetSdk = () => {
|
|
454
|
+
const name = ".NET SDK";
|
|
407
455
|
const res = run(DOTNET, ["--list-sdks"]);
|
|
408
456
|
if (!res.found) {
|
|
409
457
|
return {
|
|
410
|
-
name
|
|
458
|
+
name,
|
|
411
459
|
status: "missing",
|
|
412
460
|
detail: "`dotnet` was not found on your PATH",
|
|
413
|
-
fix:
|
|
461
|
+
fix: INSTALL_NET_10_FIX
|
|
414
462
|
};
|
|
415
463
|
}
|
|
416
464
|
if (!res.ok && !res.stdout) {
|
|
417
|
-
return {
|
|
418
|
-
name: ".NET SDK",
|
|
419
|
-
status: "unknown",
|
|
420
|
-
detail: "could not run `dotnet --list-sdks`"
|
|
421
|
-
};
|
|
465
|
+
return { name, status: "unknown", detail: "could not run `dotnet --list-sdks`" };
|
|
422
466
|
}
|
|
423
467
|
if (hasNet10Sdk(res.stdout)) {
|
|
424
468
|
const newest = newestNet10Sdk(res.stdout);
|
|
425
|
-
return {
|
|
426
|
-
name: ".NET SDK",
|
|
427
|
-
status: "ok",
|
|
428
|
-
detail: newest ? `found ${newest}` : "found 10.x"
|
|
429
|
-
};
|
|
469
|
+
return { name, status: "ok", detail: newest ? `found ${newest}` : "found 10.x" };
|
|
430
470
|
}
|
|
431
471
|
return {
|
|
432
|
-
name
|
|
472
|
+
name,
|
|
433
473
|
status: "missing",
|
|
434
474
|
detail: "no 10.x SDK installed",
|
|
435
|
-
fix:
|
|
475
|
+
fix: INSTALL_NET_10_FIX
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
var checkMauiWorkload = (workloadList) => {
|
|
479
|
+
const name = ".NET MAUI workload";
|
|
480
|
+
if (!workloadList) {
|
|
481
|
+
return { name, status: "unknown", detail: "requires the .NET SDK first" };
|
|
482
|
+
}
|
|
483
|
+
if (!workloadList.found) {
|
|
484
|
+
return { name, status: "unknown", detail: "could not query workloads" };
|
|
485
|
+
}
|
|
486
|
+
if (outputMentionsMaui(workloadList.stdout)) {
|
|
487
|
+
return { name, status: "ok", detail: "installed" };
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
name,
|
|
491
|
+
status: "missing",
|
|
492
|
+
detail: "not installed",
|
|
493
|
+
fix: "dotnet workload install maui"
|
|
436
494
|
};
|
|
437
495
|
};
|
|
438
|
-
var
|
|
439
|
-
|
|
496
|
+
var checkCSharpHotReload = (workloadList) => {
|
|
497
|
+
const name = "C# hot reload";
|
|
498
|
+
if (!workloadList) {
|
|
499
|
+
return { name, status: "unknown", detail: "requires the .NET SDK first" };
|
|
500
|
+
}
|
|
501
|
+
if (process.platform !== "darwin") {
|
|
502
|
+
return { name, status: "ok", detail: "dotnet watch supported" };
|
|
503
|
+
}
|
|
504
|
+
const sdkVersion = run(DOTNET, ["--version"]).stdout.trim();
|
|
505
|
+
if (sdkVersion && !sdkSupportsCSharpHotReload(sdkVersion)) {
|
|
440
506
|
return {
|
|
441
|
-
name
|
|
507
|
+
name,
|
|
442
508
|
status: "unknown",
|
|
443
|
-
detail:
|
|
509
|
+
detail: `SDK ${sdkVersion} \u2014 its dotnet-watch crashes Mac Catalyst apps on launch (needs 10.0.300+) \u2014 vidra dev falls back to a classic launch`,
|
|
510
|
+
fix: INSTALL_NET_10_FIX
|
|
444
511
|
};
|
|
445
512
|
}
|
|
446
|
-
const
|
|
447
|
-
if (!
|
|
513
|
+
const version = workloadSetVersion(workloadList.stdout);
|
|
514
|
+
if (!version) {
|
|
448
515
|
return {
|
|
449
|
-
name
|
|
516
|
+
name,
|
|
450
517
|
status: "unknown",
|
|
451
|
-
detail: "could not
|
|
518
|
+
detail: "could not read the workload set version"
|
|
452
519
|
};
|
|
453
520
|
}
|
|
454
|
-
if (
|
|
455
|
-
return { name
|
|
521
|
+
if (workloadSetSupportsCSharpHotReload(version)) {
|
|
522
|
+
return { name, status: "ok", detail: `workload set ${version}` };
|
|
456
523
|
}
|
|
457
524
|
return {
|
|
458
|
-
name
|
|
459
|
-
status: "
|
|
460
|
-
detail:
|
|
461
|
-
fix: "dotnet workload
|
|
525
|
+
name,
|
|
526
|
+
status: "unknown",
|
|
527
|
+
detail: `workload set ${version} predates Mac Catalyst dotnet-watch support (needs 10.0.203+) \u2014 vidra dev falls back to a classic launch`,
|
|
528
|
+
fix: "dotnet workload update"
|
|
462
529
|
};
|
|
463
530
|
};
|
|
464
531
|
var checkXcode = () => {
|
|
532
|
+
const name = "Xcode";
|
|
465
533
|
const res = run("xcode-select", ["-p"]);
|
|
466
534
|
if (!res.found || !res.ok) {
|
|
467
535
|
return {
|
|
468
|
-
name
|
|
536
|
+
name,
|
|
469
537
|
status: "missing",
|
|
470
538
|
detail: "not found",
|
|
471
539
|
fix: "Install Xcode from the App Store"
|
|
472
540
|
};
|
|
473
541
|
}
|
|
474
542
|
const devDir = res.stdout.trim();
|
|
475
|
-
if (
|
|
543
|
+
if (COMMAND_LINE_TOOLS_PATH.test(devDir)) {
|
|
476
544
|
return {
|
|
477
|
-
name
|
|
545
|
+
name,
|
|
478
546
|
status: "missing",
|
|
479
547
|
detail: "only Command Line Tools detected (Mac Catalyst needs full Xcode)",
|
|
480
548
|
fix: "Install Xcode, then: sudo xcode-select -s /Applications/Xcode.app"
|
|
481
549
|
};
|
|
482
550
|
}
|
|
483
|
-
return { name
|
|
551
|
+
return { name, status: "ok", detail: devDir };
|
|
484
552
|
};
|
|
485
553
|
var isMauiWorkloadInstalled = () => outputMentionsMaui(run(DOTNET, ["workload", "list"]).stdout);
|
|
486
554
|
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
487
555
|
var collectRequirements = (opts = {}) => {
|
|
488
556
|
const dotnet = checkDotnetSdk();
|
|
489
|
-
const
|
|
557
|
+
const workloadList = dotnet.status === "ok" ? run(DOTNET, ["workload", "list"]) : null;
|
|
558
|
+
const reqs = [
|
|
559
|
+
dotnet,
|
|
560
|
+
checkMauiWorkload(workloadList),
|
|
561
|
+
checkCSharpHotReload(workloadList)
|
|
562
|
+
];
|
|
490
563
|
if (opts.includeXcode ?? process.platform === "darwin") {
|
|
491
564
|
reqs.push(checkXcode());
|
|
492
565
|
}
|
|
@@ -508,7 +581,7 @@ var printRequirements = (reqs) => {
|
|
|
508
581
|
detail: r.detail ? dim(r.detail) : void 0
|
|
509
582
|
})
|
|
510
583
|
);
|
|
511
|
-
if (r.status
|
|
584
|
+
if (r.status !== "ok" && r.fix) {
|
|
512
585
|
console.log(fixLine(r.fix));
|
|
513
586
|
}
|
|
514
587
|
}
|
|
@@ -645,6 +718,23 @@ var printWorkloadHint = () => {
|
|
|
645
718
|
console.error(fixLine("vidra doctor", "check:"));
|
|
646
719
|
console.error();
|
|
647
720
|
};
|
|
721
|
+
var printXcodeTooOldHint = () => {
|
|
722
|
+
console.error();
|
|
723
|
+
console.error(
|
|
724
|
+
row({
|
|
725
|
+
glyph: "manual",
|
|
726
|
+
label: "your Xcode is older than the SDK this MAUI workload set expects."
|
|
727
|
+
})
|
|
728
|
+
);
|
|
729
|
+
console.error(
|
|
730
|
+
` ${dim("\u2022")} ${dim("update Xcode (App Store), then")} ${lime("sudo xcodebuild -runFirstLaunch")}`
|
|
731
|
+
);
|
|
732
|
+
console.error(
|
|
733
|
+
` ${dim("\u2022")} ${dim("or pin the workloads to your Xcode's era:")} ${lime("dotnet workload update --version <set>")}`
|
|
734
|
+
);
|
|
735
|
+
console.error(fixLine("vidra doctor", "check:"));
|
|
736
|
+
console.error();
|
|
737
|
+
};
|
|
648
738
|
var printXcodeHint = () => {
|
|
649
739
|
console.error();
|
|
650
740
|
console.error(
|
|
@@ -948,6 +1038,7 @@ var POLL_INTERVAL_MS = 500;
|
|
|
948
1038
|
var POLL_TIMEOUT_MS = 3e4;
|
|
949
1039
|
var NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
950
1040
|
var DOTNET_COMMAND = process.platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
1041
|
+
var WATCH_OUTPUT_TAIL_CHARS = 8192;
|
|
951
1042
|
var TARGETS2 = {
|
|
952
1043
|
macos: {
|
|
953
1044
|
name: "macos",
|
|
@@ -958,12 +1049,13 @@ var TARGETS2 = {
|
|
|
958
1049
|
framework: "net10.0-windows10.0.19041.0"
|
|
959
1050
|
}
|
|
960
1051
|
};
|
|
961
|
-
var devCommand = (argv) => startSession(argv, { vite: true });
|
|
962
|
-
var runCommand = (argv) => startSession(argv, { vite: false });
|
|
1052
|
+
var devCommand = (argv) => startSession(argv, { vite: true, hotReloadDefault: true });
|
|
1053
|
+
var runCommand = (argv) => startSession(argv, { vite: false, hotReloadDefault: false });
|
|
963
1054
|
var startSession = async (argv, opts) => {
|
|
964
1055
|
const args = parseArgs(["_", "_", ...argv]);
|
|
965
1056
|
const targetName = args["target"] || detectPlatform();
|
|
966
1057
|
const verbose = !!args["verbose"];
|
|
1058
|
+
let hotReload = opts.hotReloadDefault && !args["no-hot-reload"];
|
|
967
1059
|
const viteUrl = process.env.VIDRA_DEV_URL || "http://localhost:5173";
|
|
968
1060
|
const target = TARGETS2[targetName];
|
|
969
1061
|
if (!target) {
|
|
@@ -981,16 +1073,68 @@ var startSession = async (argv, opts) => {
|
|
|
981
1073
|
if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
|
|
982
1074
|
process.exit(1);
|
|
983
1075
|
}
|
|
984
|
-
|
|
1076
|
+
if (hotReload && target.name === "macos") {
|
|
1077
|
+
const blocker = macCatalystHotReloadBlocker();
|
|
1078
|
+
if (blocker) {
|
|
1079
|
+
hotReload = false;
|
|
1080
|
+
console.log();
|
|
1081
|
+
console.log(
|
|
1082
|
+
row({
|
|
1083
|
+
glyph: "manual",
|
|
1084
|
+
detail: `${dim(blocker.reason + "; using a classic launch")}`
|
|
1085
|
+
})
|
|
1086
|
+
);
|
|
1087
|
+
console.log(fixLine(blocker.fix));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const session = new DevSession(project, target, viteUrl, verbose, {
|
|
1091
|
+
vite: opts.vite,
|
|
1092
|
+
hotReload
|
|
1093
|
+
});
|
|
985
1094
|
await session.run();
|
|
986
1095
|
};
|
|
1096
|
+
var buildDotnetWatchArgs = (opts) => [
|
|
1097
|
+
"watch",
|
|
1098
|
+
"--project",
|
|
1099
|
+
opts.csprojPath,
|
|
1100
|
+
// Never prompt on rude edits (our stdio is piped, so a prompt would hang);
|
|
1101
|
+
// paired with DOTNET_WATCH_RESTART_ON_RUDE_EDIT for older SDKs.
|
|
1102
|
+
"--non-interactive",
|
|
1103
|
+
...opts.verbose ? ["--verbose"] : [],
|
|
1104
|
+
"run",
|
|
1105
|
+
"-f",
|
|
1106
|
+
opts.framework,
|
|
1107
|
+
"-c",
|
|
1108
|
+
opts.buildConfig,
|
|
1109
|
+
// Launch the Mac Catalyst app by exec'ing the binary rather than `open -a`,
|
|
1110
|
+
// so the environment (VIDRA_DEV_URL, DOTNET_STARTUP_HOOKS) propagates and
|
|
1111
|
+
// stdout streams back. Ignored by targets that don't define the property.
|
|
1112
|
+
...opts.targetName === "macos" ? ["--property:RunWithOpen=false"] : []
|
|
1113
|
+
];
|
|
1114
|
+
var dotnetWatchEnv = (devUrl) => ({
|
|
1115
|
+
VIDRA_DEV_URL: devUrl,
|
|
1116
|
+
DOTNET_WATCH_RESTART_ON_RUDE_EDIT: "1",
|
|
1117
|
+
DOTNET_WATCH_SUPPRESS_EMOJIS: "1",
|
|
1118
|
+
DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER: "1"
|
|
1119
|
+
});
|
|
1120
|
+
var HOST_READY_SENTINEL = "[vidra] host ready";
|
|
1121
|
+
var classifyWatchLine = (line) => {
|
|
1122
|
+
if (/waiting for a file to change/i.test(line)) return "appWaiting";
|
|
1123
|
+
if (/fix the error to continue/i.test(line)) return "buildBlocked";
|
|
1124
|
+
if (line.includes(HOST_READY_SENTINEL)) return "appStarted";
|
|
1125
|
+
if (/\bwatch\b/i.test(line) && /\b(?:started|launched)\b/i.test(line)) {
|
|
1126
|
+
return "appStarted";
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
};
|
|
987
1130
|
var DevSession = class {
|
|
988
|
-
constructor(project, target, viteUrl, verbose, options
|
|
1131
|
+
constructor(project, target, viteUrl, verbose, options) {
|
|
989
1132
|
this.project = project;
|
|
990
1133
|
this.target = target;
|
|
991
1134
|
this.viteUrl = viteUrl;
|
|
992
1135
|
this.verbose = verbose;
|
|
993
|
-
this.vite = options.vite
|
|
1136
|
+
this.vite = options.vite;
|
|
1137
|
+
this.hotReload = options.hotReload;
|
|
994
1138
|
}
|
|
995
1139
|
project;
|
|
996
1140
|
target;
|
|
@@ -999,7 +1143,19 @@ var DevSession = class {
|
|
|
999
1143
|
children = [];
|
|
1000
1144
|
buildConfig = process.env.VIDRA_BUILD_CONFIG || "Debug";
|
|
1001
1145
|
vite;
|
|
1146
|
+
hotReload;
|
|
1002
1147
|
shuttingDown = false;
|
|
1148
|
+
// Watch-mode state: `watchChild` is the `dotnet watch` process (a process
|
|
1149
|
+
// group leader on unix, so shutdown can signal it and the app it launched
|
|
1150
|
+
// together); `watchReady` flips once the app has started at least once.
|
|
1151
|
+
watchChild;
|
|
1152
|
+
watchReady = false;
|
|
1153
|
+
watchOutputTail = "";
|
|
1154
|
+
endSession = () => {
|
|
1155
|
+
};
|
|
1156
|
+
sessionDone = new Promise((resolve) => {
|
|
1157
|
+
this.endSession = resolve;
|
|
1158
|
+
});
|
|
1003
1159
|
async run() {
|
|
1004
1160
|
this.installSignalHandlers();
|
|
1005
1161
|
console.log();
|
|
@@ -1007,9 +1163,8 @@ var DevSession = class {
|
|
|
1007
1163
|
console.log(kv("project", this.project.projectName));
|
|
1008
1164
|
console.log(kv("target", this.target.framework));
|
|
1009
1165
|
console.log();
|
|
1010
|
-
let vite;
|
|
1011
1166
|
if (this.vite) {
|
|
1012
|
-
|
|
1167
|
+
this.startVite();
|
|
1013
1168
|
try {
|
|
1014
1169
|
await waitForServer(this.viteUrl, POLL_TIMEOUT_MS);
|
|
1015
1170
|
} catch (error) {
|
|
@@ -1028,19 +1183,22 @@ var DevSession = class {
|
|
|
1028
1183
|
)
|
|
1029
1184
|
);
|
|
1030
1185
|
}
|
|
1031
|
-
|
|
1186
|
+
this.launchHost();
|
|
1187
|
+
const hostDirLabel = `src/${path6.basename(this.project.hostDir)}`;
|
|
1032
1188
|
if (this.vite) {
|
|
1033
1189
|
console.log(
|
|
1034
1190
|
taggedRow(
|
|
1035
1191
|
"active",
|
|
1036
1192
|
null,
|
|
1037
|
-
`${lime("hot reload active")} ${dim("\u2014 edit ui/src and save")}`
|
|
1193
|
+
this.hotReload ? `${lime("hot reload active")} ${dim(`\u2014 edit ui/src or ${hostDirLabel} and save`)}` : `${lime("hot reload active")} ${dim("\u2014 edit ui/src and save")}`
|
|
1038
1194
|
)
|
|
1039
1195
|
);
|
|
1040
1196
|
console.log();
|
|
1041
1197
|
console.log(
|
|
1042
1198
|
footer(
|
|
1043
|
-
`${dim("watching")} ${value("ui/")} ${dim(
|
|
1199
|
+
this.hotReload ? `${dim("watching")} ${value("ui/")} ${dim("\xB7")} ${value(`${hostDirLabel}/`)} ${dim(
|
|
1200
|
+
"\xB7 hot reload on save \xB7 ctrl-c to stop"
|
|
1201
|
+
)}` : `${dim("watching")} ${value("ui/")} ${dim(
|
|
1044
1202
|
"\xB7 hot reload on save \xB7 ctrl-c to stop"
|
|
1045
1203
|
)}`
|
|
1046
1204
|
)
|
|
@@ -1052,7 +1210,7 @@ var DevSession = class {
|
|
|
1052
1210
|
);
|
|
1053
1211
|
}
|
|
1054
1212
|
console.log();
|
|
1055
|
-
await
|
|
1213
|
+
await this.sessionDone;
|
|
1056
1214
|
}
|
|
1057
1215
|
installSignalHandlers() {
|
|
1058
1216
|
process.on("SIGINT", () => {
|
|
@@ -1072,6 +1230,181 @@ var DevSession = class {
|
|
|
1072
1230
|
});
|
|
1073
1231
|
return this.registerChild(vite, "ui", "Vite dev server");
|
|
1074
1232
|
}
|
|
1233
|
+
launchHost() {
|
|
1234
|
+
if (this.hotReload) {
|
|
1235
|
+
return this.launchHostWithWatch();
|
|
1236
|
+
}
|
|
1237
|
+
return this.target.name === "macos" ? this.launchMacosHost() : this.launchWindowsHost();
|
|
1238
|
+
}
|
|
1239
|
+
// --- dotnet watch launch (C# hot reload) -----------------------------------
|
|
1240
|
+
launchHostWithWatch() {
|
|
1241
|
+
console.log(
|
|
1242
|
+
taggedRow(
|
|
1243
|
+
"active",
|
|
1244
|
+
"host",
|
|
1245
|
+
`${dim("dotnet watch \u2014 building")} ${value(this.target.framework)} ${dim("\u2026")}`
|
|
1246
|
+
)
|
|
1247
|
+
);
|
|
1248
|
+
const watch = spawn2(
|
|
1249
|
+
DOTNET_COMMAND,
|
|
1250
|
+
buildDotnetWatchArgs({
|
|
1251
|
+
csprojPath: this.project.csprojPath,
|
|
1252
|
+
framework: this.target.framework,
|
|
1253
|
+
buildConfig: this.buildConfig,
|
|
1254
|
+
verbose: this.verbose,
|
|
1255
|
+
targetName: this.target.name
|
|
1256
|
+
}),
|
|
1257
|
+
{
|
|
1258
|
+
cwd: this.project.root,
|
|
1259
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1260
|
+
env: { ...process.env, ...dotnetWatchEnv(this.viteUrl) },
|
|
1261
|
+
// Unix: make dotnet watch its own process-group leader so shutdown can
|
|
1262
|
+
// SIGTERM the group — taking down both watch and the app it launched.
|
|
1263
|
+
// Windows cleanup goes through `taskkill /T` instead.
|
|
1264
|
+
detached: process.platform !== "win32"
|
|
1265
|
+
}
|
|
1266
|
+
);
|
|
1267
|
+
this.watchChild = watch;
|
|
1268
|
+
this.children.push(watch);
|
|
1269
|
+
const notSentinel = (line) => !line.includes(HOST_READY_SENTINEL);
|
|
1270
|
+
prefixStream(watch.stdout, "host", notSentinel);
|
|
1271
|
+
prefixStream(watch.stderr, "host", notSentinel);
|
|
1272
|
+
scanStream(watch.stdout, (line) => this.onWatchLine(line));
|
|
1273
|
+
scanStream(watch.stderr, (line) => this.onWatchLine(line));
|
|
1274
|
+
watch.on("exit", (code, signal) => {
|
|
1275
|
+
if (this.shuttingDown) return;
|
|
1276
|
+
if (this.watchReady) {
|
|
1277
|
+
const failed = code !== null && code !== 0 || signal !== null;
|
|
1278
|
+
if (failed) {
|
|
1279
|
+
console.error(
|
|
1280
|
+
"\n" + row({
|
|
1281
|
+
glyph: "error",
|
|
1282
|
+
detail: dim(
|
|
1283
|
+
`dotnet watch exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
1284
|
+
)
|
|
1285
|
+
})
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
this.shutdown(code ?? (signal ? 1 : 0));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
this.fallBackToClassicLaunch(code, signal);
|
|
1292
|
+
});
|
|
1293
|
+
watch.on("error", (error) => {
|
|
1294
|
+
if (this.shuttingDown) return;
|
|
1295
|
+
console.error(
|
|
1296
|
+
"\n" + row({
|
|
1297
|
+
glyph: "error",
|
|
1298
|
+
detail: dim(`failed to start dotnet watch: ${error.message}`)
|
|
1299
|
+
})
|
|
1300
|
+
);
|
|
1301
|
+
this.shutdown(1);
|
|
1302
|
+
});
|
|
1303
|
+
return watch;
|
|
1304
|
+
}
|
|
1305
|
+
onWatchLine(line) {
|
|
1306
|
+
this.watchOutputTail = (this.watchOutputTail + line + "\n").slice(
|
|
1307
|
+
-WATCH_OUTPUT_TAIL_CHARS
|
|
1308
|
+
);
|
|
1309
|
+
const event = classifyWatchLine(line);
|
|
1310
|
+
if (event === "appStarted" && !this.watchReady) {
|
|
1311
|
+
this.watchReady = true;
|
|
1312
|
+
console.log(
|
|
1313
|
+
taggedRow(
|
|
1314
|
+
"done",
|
|
1315
|
+
"host",
|
|
1316
|
+
`${dim("launched")} ${value(this.project.projectName)} ${dim("\u2014 C# hot reload active")}`
|
|
1317
|
+
)
|
|
1318
|
+
);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (event !== "appWaiting" && event !== "buildBlocked" || this.shuttingDown) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (this.watchReady) {
|
|
1325
|
+
console.log(
|
|
1326
|
+
taggedRow(
|
|
1327
|
+
"manual",
|
|
1328
|
+
"host",
|
|
1329
|
+
dim("app not running \u2014 save a C# file to relaunch, or ctrl-c to stop")
|
|
1330
|
+
)
|
|
1331
|
+
);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (!/Build FAILED/i.test(this.watchOutputTail)) {
|
|
1335
|
+
console.log(
|
|
1336
|
+
taggedRow(
|
|
1337
|
+
"manual",
|
|
1338
|
+
"host",
|
|
1339
|
+
dim(
|
|
1340
|
+
"app exited before it was ready \u2014 save a C# file to relaunch, or ctrl-c to stop"
|
|
1341
|
+
)
|
|
1342
|
+
)
|
|
1343
|
+
);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (looksLikeMissingWorkload(this.watchOutputTail)) printWorkloadHint();
|
|
1347
|
+
else if (looksLikeXcodeTooOld(this.watchOutputTail)) printXcodeTooOldHint();
|
|
1348
|
+
else if (looksLikeMissingXcode(this.watchOutputTail)) printXcodeHint();
|
|
1349
|
+
console.log(
|
|
1350
|
+
taggedRow(
|
|
1351
|
+
"manual",
|
|
1352
|
+
"host",
|
|
1353
|
+
dim("build failed \u2014 fix the error and save to retry, or ctrl-c to stop")
|
|
1354
|
+
)
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* `dotnet watch` exited before the app ever started — typically an SDK or
|
|
1359
|
+
* MAUI workload set that predates `dotnet run`/`dotnet watch` support for
|
|
1360
|
+
* this target. Explain why, then launch the host the classic way (one
|
|
1361
|
+
* build + direct spawn) so the dev session still works, minus C# hot reload.
|
|
1362
|
+
*/
|
|
1363
|
+
fallBackToClassicLaunch(code, signal) {
|
|
1364
|
+
console.log();
|
|
1365
|
+
console.log(
|
|
1366
|
+
taggedRow(
|
|
1367
|
+
"manual",
|
|
1368
|
+
"host",
|
|
1369
|
+
`${dim("C# hot reload unavailable \u2014 dotnet watch exited with")} ${value(
|
|
1370
|
+
signal ? `signal ${signal}` : `code ${code ?? "unknown"}`
|
|
1371
|
+
)}`
|
|
1372
|
+
)
|
|
1373
|
+
);
|
|
1374
|
+
if (looksLikeMissingWorkload(this.watchOutputTail)) {
|
|
1375
|
+
printWorkloadHint();
|
|
1376
|
+
} else if (looksLikeXcodeTooOld(this.watchOutputTail)) {
|
|
1377
|
+
printXcodeTooOldHint();
|
|
1378
|
+
} else if (looksLikeMissingXcode(this.watchOutputTail)) {
|
|
1379
|
+
printXcodeHint();
|
|
1380
|
+
} else {
|
|
1381
|
+
console.log(
|
|
1382
|
+
footer(
|
|
1383
|
+
dim(
|
|
1384
|
+
"C# hot reload needs the .NET 10.0.203+ workload set (Mac Catalyst dotnet-watch support) \u2014 try:"
|
|
1385
|
+
)
|
|
1386
|
+
)
|
|
1387
|
+
);
|
|
1388
|
+
console.log(fixLine("dotnet workload update"));
|
|
1389
|
+
if (!this.verbose) {
|
|
1390
|
+
console.log(footer(dim("re-run with --verbose for the full watch log.")));
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
console.log(
|
|
1394
|
+
taggedRow(
|
|
1395
|
+
"active",
|
|
1396
|
+
"host",
|
|
1397
|
+
dim("falling back to a classic launch (frontend hot reload still active)\u2026")
|
|
1398
|
+
)
|
|
1399
|
+
);
|
|
1400
|
+
console.log();
|
|
1401
|
+
if (this.target.name === "macos") {
|
|
1402
|
+
this.launchMacosHost();
|
|
1403
|
+
} else {
|
|
1404
|
+
this.launchWindowsHost();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// --- classic launch (one build + direct spawn) ------------------------------
|
|
1075
1408
|
// Builds the MAUI host as a discrete step (a plain `dotnet build`, never
|
|
1076
1409
|
// MSBuild's `-t:Run`) so the per-OS launch paths can spawn the produced
|
|
1077
1410
|
// binary directly. `-t:Run` shells out in ways that break for both locally
|
|
@@ -1194,8 +1527,9 @@ var DevSession = class {
|
|
|
1194
1527
|
}
|
|
1195
1528
|
registerChild(child, tag2, label) {
|
|
1196
1529
|
this.children.push(child);
|
|
1197
|
-
|
|
1198
|
-
prefixStream(child.
|
|
1530
|
+
const include = tag2 === "host" ? (line) => !line.includes(HOST_READY_SENTINEL) : void 0;
|
|
1531
|
+
prefixStream(child.stdout, tag2, include);
|
|
1532
|
+
prefixStream(child.stderr, tag2, include);
|
|
1199
1533
|
child.on("exit", (code, signal) => {
|
|
1200
1534
|
if (this.shuttingDown) return;
|
|
1201
1535
|
if (tag2 === "ui") {
|
|
@@ -1238,8 +1572,9 @@ var DevSession = class {
|
|
|
1238
1572
|
if (this.shuttingDown) return;
|
|
1239
1573
|
this.shuttingDown = true;
|
|
1240
1574
|
for (const child of this.children) {
|
|
1241
|
-
killChild(child);
|
|
1575
|
+
killChild(child, { processGroup: child === this.watchChild });
|
|
1242
1576
|
}
|
|
1577
|
+
this.endSession();
|
|
1243
1578
|
process.exit(exitCode);
|
|
1244
1579
|
}
|
|
1245
1580
|
};
|
|
@@ -1257,19 +1592,32 @@ var ensureTargetMatchesHostOs = (targetName) => {
|
|
|
1257
1592
|
process.exit(1);
|
|
1258
1593
|
}
|
|
1259
1594
|
};
|
|
1260
|
-
var prefixStream = (stream, tag2) => {
|
|
1595
|
+
var prefixStream = (stream, tag2, include = () => true) => {
|
|
1261
1596
|
if (!stream) return;
|
|
1262
1597
|
const prefix = streamPrefix(tag2);
|
|
1263
1598
|
stream.on("data", (chunk) => {
|
|
1264
1599
|
const lines = chunk.toString().split("\n");
|
|
1265
1600
|
for (const line of lines) {
|
|
1266
|
-
if (line.length > 0) {
|
|
1601
|
+
if (line.length > 0 && include(line)) {
|
|
1267
1602
|
process.stdout.write(`${prefix} ${line}
|
|
1268
1603
|
`);
|
|
1269
1604
|
}
|
|
1270
1605
|
}
|
|
1271
1606
|
});
|
|
1272
1607
|
};
|
|
1608
|
+
var scanStream = (stream, onLine) => {
|
|
1609
|
+
if (!stream) return;
|
|
1610
|
+
let pending = "";
|
|
1611
|
+
stream.on("data", (chunk) => {
|
|
1612
|
+
pending += chunk.toString();
|
|
1613
|
+
const lines = pending.split("\n");
|
|
1614
|
+
pending = lines.pop() ?? "";
|
|
1615
|
+
for (const line of lines) onLine(line);
|
|
1616
|
+
});
|
|
1617
|
+
stream.on("end", () => {
|
|
1618
|
+
if (pending) onLine(pending);
|
|
1619
|
+
});
|
|
1620
|
+
};
|
|
1273
1621
|
var waitForServer = (url, timeoutMs) => {
|
|
1274
1622
|
const { hostname, port, pathname } = new URL(url);
|
|
1275
1623
|
const start = Date.now();
|
|
@@ -1301,14 +1649,6 @@ var waitForServer = (url, timeoutMs) => {
|
|
|
1301
1649
|
poll();
|
|
1302
1650
|
});
|
|
1303
1651
|
};
|
|
1304
|
-
var waitForExit = (...children) => {
|
|
1305
|
-
return new Promise((resolve) => {
|
|
1306
|
-
const resolveOnce = () => resolve();
|
|
1307
|
-
for (const child of children) {
|
|
1308
|
-
child.once("exit", resolveOnce);
|
|
1309
|
-
}
|
|
1310
|
-
});
|
|
1311
|
-
};
|
|
1312
1652
|
var findMacAppBundle = (hostDir, framework, buildConfig) => {
|
|
1313
1653
|
const outputDir = path6.join(hostDir, "bin", buildConfig, framework);
|
|
1314
1654
|
return findAppBundleRecursive(outputDir);
|
|
@@ -1362,7 +1702,7 @@ var findFileRecursive = (dir, match) => {
|
|
|
1362
1702
|
}
|
|
1363
1703
|
return null;
|
|
1364
1704
|
};
|
|
1365
|
-
var killChild = (child) => {
|
|
1705
|
+
var killChild = (child, opts = {}) => {
|
|
1366
1706
|
if (!child.pid || child.exitCode !== null) return;
|
|
1367
1707
|
if (process.platform === "win32") {
|
|
1368
1708
|
try {
|
|
@@ -1374,6 +1714,13 @@ var killChild = (child) => {
|
|
|
1374
1714
|
}
|
|
1375
1715
|
return;
|
|
1376
1716
|
}
|
|
1717
|
+
if (opts.processGroup) {
|
|
1718
|
+
try {
|
|
1719
|
+
process.kill(-child.pid, "SIGTERM");
|
|
1720
|
+
return;
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1377
1724
|
child.kill("SIGTERM");
|
|
1378
1725
|
};
|
|
1379
1726
|
var printMacLaunchHint = () => {
|
|
@@ -1411,7 +1758,7 @@ var printHelp = () => {
|
|
|
1411
1758
|
${lime("vidra")} ${dim("<command> [options]")}
|
|
1412
1759
|
|
|
1413
1760
|
${dim("commands")}
|
|
1414
|
-
${cmd("dev", "start vite + the native host (hot reload)")}
|
|
1761
|
+
${cmd("dev", "start vite + the native host (UI + C# hot reload)")}
|
|
1415
1762
|
${cmd("run", "launch the native host only")}
|
|
1416
1763
|
${cmd("build", "build & package for distribution")}
|
|
1417
1764
|
${cmd("doctor", "check your environment")}
|
|
@@ -1419,6 +1766,7 @@ ${cmd("help", "show this message")}
|
|
|
1419
1766
|
|
|
1420
1767
|
${dim("examples")}
|
|
1421
1768
|
${ex("dev --target windows", "run the windows host")}
|
|
1769
|
+
${ex("dev --no-hot-reload", "skip dotnet watch, classic launch")}
|
|
1422
1770
|
${ex("build --plan", "preview the build, run nothing")}
|
|
1423
1771
|
${ex("build --target macos", "build & package a macOS DMG")}
|
|
1424
1772
|
${ex("doctor", "verify .NET SDK + MAUI workload")}
|
package/dist/index.js
CHANGED
|
@@ -174,6 +174,7 @@ import { execFileSync } from "child_process";
|
|
|
174
174
|
import prompts from "prompts";
|
|
175
175
|
var DOTNET = process.platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
176
176
|
var MAUI_DOCS = "https://learn.microsoft.com/dotnet/maui/get-started/installation";
|
|
177
|
+
var INSTALL_NET_10_FIX = "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download";
|
|
177
178
|
var bufToStr = (v) => v == null ? "" : Buffer.isBuffer(v) ? v.toString() : v;
|
|
178
179
|
var run = (cmd, args) => {
|
|
179
180
|
try {
|
|
@@ -192,41 +193,37 @@ var run = (cmd, args) => {
|
|
|
192
193
|
};
|
|
193
194
|
}
|
|
194
195
|
};
|
|
195
|
-
var
|
|
196
|
-
var
|
|
196
|
+
var splitLines = (text) => text.split(/\r?\n/);
|
|
197
|
+
var NET_10_VERSION = /^10\./;
|
|
198
|
+
var MAUI_WORKLOAD = /\bmaui\b/i;
|
|
199
|
+
var hasNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).some((line) => NET_10_VERSION.test(line.trim()));
|
|
200
|
+
var newestNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).map((line) => line.trim().split(/\s+/)[0]).filter((version) => NET_10_VERSION.test(version)).sort(
|
|
197
201
|
(a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" })
|
|
198
202
|
).pop();
|
|
199
|
-
var outputMentionsMaui = (workloadListOutput) =>
|
|
203
|
+
var outputMentionsMaui = (workloadListOutput) => MAUI_WORKLOAD.test(workloadListOutput);
|
|
200
204
|
var checkDotnetSdk = () => {
|
|
205
|
+
const name = ".NET SDK";
|
|
201
206
|
const res = run(DOTNET, ["--list-sdks"]);
|
|
202
207
|
if (!res.found) {
|
|
203
208
|
return {
|
|
204
|
-
name
|
|
209
|
+
name,
|
|
205
210
|
status: "missing",
|
|
206
211
|
detail: "`dotnet` was not found on your PATH",
|
|
207
|
-
fix:
|
|
212
|
+
fix: INSTALL_NET_10_FIX
|
|
208
213
|
};
|
|
209
214
|
}
|
|
210
215
|
if (!res.ok && !res.stdout) {
|
|
211
|
-
return {
|
|
212
|
-
name: ".NET SDK",
|
|
213
|
-
status: "unknown",
|
|
214
|
-
detail: "could not run `dotnet --list-sdks`"
|
|
215
|
-
};
|
|
216
|
+
return { name, status: "unknown", detail: "could not run `dotnet --list-sdks`" };
|
|
216
217
|
}
|
|
217
218
|
if (hasNet10Sdk(res.stdout)) {
|
|
218
219
|
const newest = newestNet10Sdk(res.stdout);
|
|
219
|
-
return {
|
|
220
|
-
name: ".NET SDK",
|
|
221
|
-
status: "ok",
|
|
222
|
-
detail: newest ? `found ${newest}` : "found 10.x"
|
|
223
|
-
};
|
|
220
|
+
return { name, status: "ok", detail: newest ? `found ${newest}` : "found 10.x" };
|
|
224
221
|
}
|
|
225
222
|
return {
|
|
226
|
-
name
|
|
223
|
+
name,
|
|
227
224
|
status: "missing",
|
|
228
225
|
detail: "no 10.x SDK installed",
|
|
229
|
-
fix:
|
|
226
|
+
fix: INSTALL_NET_10_FIX
|
|
230
227
|
};
|
|
231
228
|
};
|
|
232
229
|
var isMauiWorkloadInstalled = () => outputMentionsMaui(run(DOTNET, ["workload", "list"]).stdout);
|
|
@@ -332,7 +329,7 @@ var VIDRA_REPO_ROOT = path2.resolve(CLI_ROOT, "..", "..", "..");
|
|
|
332
329
|
var LOCAL_FEED_DIR = path2.join(VIDRA_REPO_ROOT, "dist", "packages");
|
|
333
330
|
var LOCAL_CLI_DIR = CLI_ROOT;
|
|
334
331
|
var LOCAL_SDK_DIR = path2.join(VIDRA_REPO_ROOT, "src", "sdk", "vidra-js");
|
|
335
|
-
var VIDRA_VERSION = "0.2.
|
|
332
|
+
var VIDRA_VERSION = "0.2.2";
|
|
336
333
|
var SDK_VERSION = "0.1.0";
|
|
337
334
|
var main = async () => {
|
|
338
335
|
console.log();
|
package/package.json
CHANGED
|
@@ -30,7 +30,22 @@ exact command to fix anything that's missing.
|
|
|
30
30
|
npm run dev
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
This starts the Vite dev server and launches the native host for the current OS
|
|
33
|
+
This starts the Vite dev server and launches the native host for the current OS
|
|
34
|
+
under `dotnet watch`. Both sides of the app hot reload:
|
|
35
|
+
|
|
36
|
+
- **UI**: edit anything in `ui/src` — Vite HMR updates the WebView instantly.
|
|
37
|
+
- **C#**: edit the host (for example `OnTickAsync` in `MainPage.cs`) — supported
|
|
38
|
+
edits apply to the running app in seconds, and the UI flashes a
|
|
39
|
+
"C# reloaded" badge. Edits hot reload can't express (new fields, changed
|
|
40
|
+
signatures, …) trigger an automatic rebuild and relaunch.
|
|
41
|
+
|
|
42
|
+
C# hot reload needs a recent toolchain (on macOS, the .NET 10.0.203+ workload
|
|
43
|
+
set — run `npm run doctor` to check). When unavailable, `vidra dev` falls back
|
|
44
|
+
to a one-shot build and launch. To skip `dotnet watch` explicitly:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx vidra dev --no-hot-reload
|
|
48
|
+
```
|
|
34
49
|
|
|
35
50
|
To target a specific desktop platform explicitly:
|
|
36
51
|
|
|
@@ -14,15 +14,20 @@ public class MainPage : VidraPage
|
|
|
14
14
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
|
|
15
15
|
while (await timer.WaitForNextTickAsync())
|
|
16
16
|
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
await OnTickAsync();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async Task OnTickAsync()
|
|
22
|
+
{
|
|
23
|
+
try
|
|
24
|
+
{
|
|
25
|
+
var count = await Bridge.CallJsAsync<int>("counter.increment");
|
|
26
|
+
System.Diagnostics.Debug.WriteLine($"[MainPage] Counter is now {count}");
|
|
27
|
+
}
|
|
28
|
+
catch (Exception ex)
|
|
29
|
+
{
|
|
30
|
+
System.Diagnostics.Debug.WriteLine($"[MainPage] Counter increment failed: {ex.Message}");
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
}
|
|
@@ -19,7 +19,9 @@ const App = () => {
|
|
|
19
19
|
const [windowSupport, setWindowSupport] = useState<WindowSupport | null>(null);
|
|
20
20
|
const [caps, setCaps] = useState<Record<string, string[]> | null>(null);
|
|
21
21
|
const [count, setCount] = useState(0);
|
|
22
|
+
const [csReloaded, setCsReloaded] = useState<string | null>(null);
|
|
22
23
|
const countRef = useRef(0);
|
|
24
|
+
const reloadFlashTimer = useRef<number | undefined>(undefined);
|
|
23
25
|
|
|
24
26
|
useEffect(() => {
|
|
25
27
|
const unsubscribeCounter = vidra.handle<void, number>("counter.increment", () => {
|
|
@@ -28,6 +30,20 @@ const App = () => {
|
|
|
28
30
|
return countRef.current;
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
// Emitted by the native host when `vidra dev` hot reloads edited C#.
|
|
34
|
+
const unsubscribeHotReloaded = vidra.on<{ updatedTypes?: string[] }>(
|
|
35
|
+
"vidra.hotReloaded",
|
|
36
|
+
(data) => {
|
|
37
|
+
const typeName = data?.updatedTypes?.[0]?.split(".").pop();
|
|
38
|
+
setCsReloaded(typeName ? `C# reloaded · ${typeName}` : "C# reloaded");
|
|
39
|
+
window.clearTimeout(reloadFlashTimer.current);
|
|
40
|
+
reloadFlashTimer.current = window.setTimeout(
|
|
41
|
+
() => setCsReloaded(null),
|
|
42
|
+
2500,
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
31
47
|
const unsubscribeResized = appWindow.onResized((windowInfo) => {
|
|
32
48
|
setWindowSummary(`Window resized: ${describeWindow(windowInfo)}`);
|
|
33
49
|
});
|
|
@@ -47,8 +63,10 @@ const App = () => {
|
|
|
47
63
|
|
|
48
64
|
return () => {
|
|
49
65
|
unsubscribeCounter();
|
|
66
|
+
unsubscribeHotReloaded();
|
|
50
67
|
unsubscribeResized();
|
|
51
68
|
unsubscribeStateChanged();
|
|
69
|
+
window.clearTimeout(reloadFlashTimer.current);
|
|
52
70
|
};
|
|
53
71
|
}, []);
|
|
54
72
|
|
|
@@ -200,6 +218,12 @@ const App = () => {
|
|
|
200
218
|
<span className="dot" />
|
|
201
219
|
React + .NET MAUI
|
|
202
220
|
</span>
|
|
221
|
+
{csReloaded && (
|
|
222
|
+
<span className="badge reload-badge">
|
|
223
|
+
<span className="dot" />
|
|
224
|
+
{csReloaded}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
203
227
|
<h1 className="title">{{appTitle}}</h1>
|
|
204
228
|
<p className="tagline">
|
|
205
229
|
A cross-platform desktop app with a native bridge.
|
|
@@ -146,6 +146,22 @@ body::before {
|
|
|
146
146
|
box-shadow: 0 0 0 3px var(--dot-glow);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/* Flashes when the native host hot reloads edited C# (vidra.hotReloaded). */
|
|
150
|
+
.reload-badge {
|
|
151
|
+
margin-left: 0.5rem;
|
|
152
|
+
color: var(--accent);
|
|
153
|
+
background: var(--accent-soft);
|
|
154
|
+
border-color: var(--accent-border);
|
|
155
|
+
animation: reload-pop 0.25s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@keyframes reload-pop {
|
|
159
|
+
from {
|
|
160
|
+
opacity: 0;
|
|
161
|
+
transform: scale(0.9);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
.title {
|
|
150
166
|
margin-top: 1.1rem;
|
|
151
167
|
font-size: clamp(2.4rem, 6vw, 3.4rem);
|