create-vidra-app 0.1.9 → 0.2.1
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 +380 -70
- 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 +40 -3
- 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,156 @@ 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 meetsMinimumVersion = (version, minimum) => {
|
|
404
|
+
const segments = version.split(".").map((s) => Number.parseInt(s, 10));
|
|
405
|
+
if (segments.slice(0, minimum.length).some(Number.isNaN)) return false;
|
|
406
|
+
for (let i = 0; i < minimum.length; i++) {
|
|
407
|
+
const segment = segments[i] ?? 0;
|
|
408
|
+
if (segment !== minimum[i]) return segment > minimum[i];
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
};
|
|
412
|
+
var workloadSetSupportsCSharpHotReload = (version) => meetsMinimumVersion(version, MIN_MACCATALYST_WATCH_WORKLOAD_SET);
|
|
413
|
+
var macCatalystHotReloadBlocker = () => {
|
|
414
|
+
const res = run(DOTNET, ["workload", "list"]);
|
|
415
|
+
if (!res.found) return null;
|
|
416
|
+
const version = workloadSetVersion(res.stdout);
|
|
417
|
+
if (!version) return null;
|
|
418
|
+
return workloadSetSupportsCSharpHotReload(version) ? null : version;
|
|
419
|
+
};
|
|
420
|
+
var MISSING_WORKLOAD_SIGNATURES = [
|
|
395
421
|
/NETSDK1147/i,
|
|
396
422
|
/workloads?\s+must\s+be\s+installed/i,
|
|
397
423
|
/maui-maccatalyst/i,
|
|
398
424
|
/maui-windows/i,
|
|
399
425
|
/to\s+install\s+the\s+.*workload/i
|
|
400
|
-
]
|
|
401
|
-
var
|
|
426
|
+
];
|
|
427
|
+
var MISSING_XCODE_SIGNATURES = [
|
|
402
428
|
/valid\s+Xcode\s+installation\s+was\s+not\s+found/i,
|
|
403
429
|
/could\s+not\s+find\s+a\s+valid\s+Xcode\s+app\s+bundle/i,
|
|
404
430
|
/macios-missing-xcode/i
|
|
405
|
-
]
|
|
431
|
+
];
|
|
432
|
+
var OUTDATED_XCODE_SIGNATURES = [
|
|
433
|
+
/error\s+MT0180/i,
|
|
434
|
+
/requires\s+the\s+MacCatalyst\s+\S+\s+SDK\s+\(shipped\s+with\s+Xcode/i
|
|
435
|
+
];
|
|
436
|
+
var looksLikeMissingWorkload = (output) => matchesAny(output, MISSING_WORKLOAD_SIGNATURES);
|
|
437
|
+
var looksLikeMissingXcode = (output) => matchesAny(output, MISSING_XCODE_SIGNATURES);
|
|
438
|
+
var looksLikeXcodeTooOld = (output) => matchesAny(output, OUTDATED_XCODE_SIGNATURES);
|
|
406
439
|
var checkDotnetSdk = () => {
|
|
440
|
+
const name = ".NET SDK";
|
|
407
441
|
const res = run(DOTNET, ["--list-sdks"]);
|
|
408
442
|
if (!res.found) {
|
|
409
443
|
return {
|
|
410
|
-
name
|
|
444
|
+
name,
|
|
411
445
|
status: "missing",
|
|
412
446
|
detail: "`dotnet` was not found on your PATH",
|
|
413
|
-
fix:
|
|
447
|
+
fix: INSTALL_NET_10_FIX
|
|
414
448
|
};
|
|
415
449
|
}
|
|
416
450
|
if (!res.ok && !res.stdout) {
|
|
417
|
-
return {
|
|
418
|
-
name: ".NET SDK",
|
|
419
|
-
status: "unknown",
|
|
420
|
-
detail: "could not run `dotnet --list-sdks`"
|
|
421
|
-
};
|
|
451
|
+
return { name, status: "unknown", detail: "could not run `dotnet --list-sdks`" };
|
|
422
452
|
}
|
|
423
453
|
if (hasNet10Sdk(res.stdout)) {
|
|
424
454
|
const newest = newestNet10Sdk(res.stdout);
|
|
425
|
-
return {
|
|
426
|
-
name: ".NET SDK",
|
|
427
|
-
status: "ok",
|
|
428
|
-
detail: newest ? `found ${newest}` : "found 10.x"
|
|
429
|
-
};
|
|
455
|
+
return { name, status: "ok", detail: newest ? `found ${newest}` : "found 10.x" };
|
|
430
456
|
}
|
|
431
457
|
return {
|
|
432
|
-
name
|
|
458
|
+
name,
|
|
433
459
|
status: "missing",
|
|
434
460
|
detail: "no 10.x SDK installed",
|
|
435
|
-
fix:
|
|
461
|
+
fix: INSTALL_NET_10_FIX
|
|
436
462
|
};
|
|
437
463
|
};
|
|
438
|
-
var checkMauiWorkload = (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
status: "unknown",
|
|
443
|
-
detail: "requires the .NET SDK first"
|
|
444
|
-
};
|
|
464
|
+
var checkMauiWorkload = (workloadList) => {
|
|
465
|
+
const name = ".NET MAUI workload";
|
|
466
|
+
if (!workloadList) {
|
|
467
|
+
return { name, status: "unknown", detail: "requires the .NET SDK first" };
|
|
445
468
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
name: ".NET MAUI workload",
|
|
450
|
-
status: "unknown",
|
|
451
|
-
detail: "could not query workloads"
|
|
452
|
-
};
|
|
469
|
+
if (!workloadList.found) {
|
|
470
|
+
return { name, status: "unknown", detail: "could not query workloads" };
|
|
453
471
|
}
|
|
454
|
-
if (outputMentionsMaui(
|
|
455
|
-
return { name
|
|
472
|
+
if (outputMentionsMaui(workloadList.stdout)) {
|
|
473
|
+
return { name, status: "ok", detail: "installed" };
|
|
456
474
|
}
|
|
457
475
|
return {
|
|
458
|
-
name
|
|
476
|
+
name,
|
|
459
477
|
status: "missing",
|
|
460
478
|
detail: "not installed",
|
|
461
479
|
fix: "dotnet workload install maui"
|
|
462
480
|
};
|
|
463
481
|
};
|
|
482
|
+
var checkCSharpHotReload = (workloadList) => {
|
|
483
|
+
const name = "C# hot reload";
|
|
484
|
+
if (!workloadList) {
|
|
485
|
+
return { name, status: "unknown", detail: "requires the .NET SDK first" };
|
|
486
|
+
}
|
|
487
|
+
if (process.platform !== "darwin") {
|
|
488
|
+
return { name, status: "ok", detail: "dotnet watch supported" };
|
|
489
|
+
}
|
|
490
|
+
const version = workloadSetVersion(workloadList.stdout);
|
|
491
|
+
if (!version) {
|
|
492
|
+
return {
|
|
493
|
+
name,
|
|
494
|
+
status: "unknown",
|
|
495
|
+
detail: "could not read the workload set version"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (workloadSetSupportsCSharpHotReload(version)) {
|
|
499
|
+
return { name, status: "ok", detail: `workload set ${version}` };
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
name,
|
|
503
|
+
status: "unknown",
|
|
504
|
+
detail: `workload set ${version} predates Mac Catalyst dotnet-watch support (needs 10.0.203+) \u2014 vidra dev falls back to a classic launch`,
|
|
505
|
+
fix: "dotnet workload update"
|
|
506
|
+
};
|
|
507
|
+
};
|
|
464
508
|
var checkXcode = () => {
|
|
509
|
+
const name = "Xcode";
|
|
465
510
|
const res = run("xcode-select", ["-p"]);
|
|
466
511
|
if (!res.found || !res.ok) {
|
|
467
512
|
return {
|
|
468
|
-
name
|
|
513
|
+
name,
|
|
469
514
|
status: "missing",
|
|
470
515
|
detail: "not found",
|
|
471
516
|
fix: "Install Xcode from the App Store"
|
|
472
517
|
};
|
|
473
518
|
}
|
|
474
519
|
const devDir = res.stdout.trim();
|
|
475
|
-
if (
|
|
520
|
+
if (COMMAND_LINE_TOOLS_PATH.test(devDir)) {
|
|
476
521
|
return {
|
|
477
|
-
name
|
|
522
|
+
name,
|
|
478
523
|
status: "missing",
|
|
479
524
|
detail: "only Command Line Tools detected (Mac Catalyst needs full Xcode)",
|
|
480
525
|
fix: "Install Xcode, then: sudo xcode-select -s /Applications/Xcode.app"
|
|
481
526
|
};
|
|
482
527
|
}
|
|
483
|
-
return { name
|
|
528
|
+
return { name, status: "ok", detail: devDir };
|
|
484
529
|
};
|
|
485
530
|
var isMauiWorkloadInstalled = () => outputMentionsMaui(run(DOTNET, ["workload", "list"]).stdout);
|
|
486
531
|
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
487
532
|
var collectRequirements = (opts = {}) => {
|
|
488
533
|
const dotnet = checkDotnetSdk();
|
|
489
|
-
const
|
|
534
|
+
const workloadList = dotnet.status === "ok" ? run(DOTNET, ["workload", "list"]) : null;
|
|
535
|
+
const reqs = [
|
|
536
|
+
dotnet,
|
|
537
|
+
checkMauiWorkload(workloadList),
|
|
538
|
+
checkCSharpHotReload(workloadList)
|
|
539
|
+
];
|
|
490
540
|
if (opts.includeXcode ?? process.platform === "darwin") {
|
|
491
541
|
reqs.push(checkXcode());
|
|
492
542
|
}
|
|
@@ -508,7 +558,7 @@ var printRequirements = (reqs) => {
|
|
|
508
558
|
detail: r.detail ? dim(r.detail) : void 0
|
|
509
559
|
})
|
|
510
560
|
);
|
|
511
|
-
if (r.status
|
|
561
|
+
if (r.status !== "ok" && r.fix) {
|
|
512
562
|
console.log(fixLine(r.fix));
|
|
513
563
|
}
|
|
514
564
|
}
|
|
@@ -645,6 +695,23 @@ var printWorkloadHint = () => {
|
|
|
645
695
|
console.error(fixLine("vidra doctor", "check:"));
|
|
646
696
|
console.error();
|
|
647
697
|
};
|
|
698
|
+
var printXcodeTooOldHint = () => {
|
|
699
|
+
console.error();
|
|
700
|
+
console.error(
|
|
701
|
+
row({
|
|
702
|
+
glyph: "manual",
|
|
703
|
+
label: "your Xcode is older than the SDK this MAUI workload set expects."
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
console.error(
|
|
707
|
+
` ${dim("\u2022")} ${dim("update Xcode (App Store), then")} ${lime("sudo xcodebuild -runFirstLaunch")}`
|
|
708
|
+
);
|
|
709
|
+
console.error(
|
|
710
|
+
` ${dim("\u2022")} ${dim("or pin the workloads to your Xcode's era:")} ${lime("dotnet workload update --version <set>")}`
|
|
711
|
+
);
|
|
712
|
+
console.error(fixLine("vidra doctor", "check:"));
|
|
713
|
+
console.error();
|
|
714
|
+
};
|
|
648
715
|
var printXcodeHint = () => {
|
|
649
716
|
console.error();
|
|
650
717
|
console.error(
|
|
@@ -948,6 +1015,7 @@ var POLL_INTERVAL_MS = 500;
|
|
|
948
1015
|
var POLL_TIMEOUT_MS = 3e4;
|
|
949
1016
|
var NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
950
1017
|
var DOTNET_COMMAND = process.platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
1018
|
+
var WATCH_OUTPUT_TAIL_CHARS = 8192;
|
|
951
1019
|
var TARGETS2 = {
|
|
952
1020
|
macos: {
|
|
953
1021
|
name: "macos",
|
|
@@ -958,12 +1026,13 @@ var TARGETS2 = {
|
|
|
958
1026
|
framework: "net10.0-windows10.0.19041.0"
|
|
959
1027
|
}
|
|
960
1028
|
};
|
|
961
|
-
var devCommand = (argv) => startSession(argv, { vite: true });
|
|
962
|
-
var runCommand = (argv) => startSession(argv, { vite: false });
|
|
1029
|
+
var devCommand = (argv) => startSession(argv, { vite: true, hotReloadDefault: true });
|
|
1030
|
+
var runCommand = (argv) => startSession(argv, { vite: false, hotReloadDefault: false });
|
|
963
1031
|
var startSession = async (argv, opts) => {
|
|
964
1032
|
const args = parseArgs(["_", "_", ...argv]);
|
|
965
1033
|
const targetName = args["target"] || detectPlatform();
|
|
966
1034
|
const verbose = !!args["verbose"];
|
|
1035
|
+
let hotReload = opts.hotReloadDefault && !args["no-hot-reload"];
|
|
967
1036
|
const viteUrl = process.env.VIDRA_DEV_URL || "http://localhost:5173";
|
|
968
1037
|
const target = TARGETS2[targetName];
|
|
969
1038
|
if (!target) {
|
|
@@ -981,16 +1050,68 @@ var startSession = async (argv, opts) => {
|
|
|
981
1050
|
if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
|
|
982
1051
|
process.exit(1);
|
|
983
1052
|
}
|
|
984
|
-
|
|
1053
|
+
if (hotReload && target.name === "macos") {
|
|
1054
|
+
const blocker = macCatalystHotReloadBlocker();
|
|
1055
|
+
if (blocker) {
|
|
1056
|
+
hotReload = false;
|
|
1057
|
+
console.log();
|
|
1058
|
+
console.log(
|
|
1059
|
+
row({
|
|
1060
|
+
glyph: "manual",
|
|
1061
|
+
detail: `${dim("C# hot reload needs workload set 10.0.203+ \u2014 found")} ${value(blocker)}${dim("; using a classic launch")}`
|
|
1062
|
+
})
|
|
1063
|
+
);
|
|
1064
|
+
console.log(fixLine("dotnet workload update"));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const session = new DevSession(project, target, viteUrl, verbose, {
|
|
1068
|
+
vite: opts.vite,
|
|
1069
|
+
hotReload
|
|
1070
|
+
});
|
|
985
1071
|
await session.run();
|
|
986
1072
|
};
|
|
1073
|
+
var buildDotnetWatchArgs = (opts) => [
|
|
1074
|
+
"watch",
|
|
1075
|
+
"--project",
|
|
1076
|
+
opts.csprojPath,
|
|
1077
|
+
// Never prompt on rude edits (our stdio is piped, so a prompt would hang);
|
|
1078
|
+
// paired with DOTNET_WATCH_RESTART_ON_RUDE_EDIT for older SDKs.
|
|
1079
|
+
"--non-interactive",
|
|
1080
|
+
...opts.verbose ? ["--verbose"] : [],
|
|
1081
|
+
"run",
|
|
1082
|
+
"-f",
|
|
1083
|
+
opts.framework,
|
|
1084
|
+
"-c",
|
|
1085
|
+
opts.buildConfig,
|
|
1086
|
+
// Launch the Mac Catalyst app by exec'ing the binary rather than `open -a`,
|
|
1087
|
+
// so the environment (VIDRA_DEV_URL, DOTNET_STARTUP_HOOKS) propagates and
|
|
1088
|
+
// stdout streams back. Ignored by targets that don't define the property.
|
|
1089
|
+
...opts.targetName === "macos" ? ["--property:RunWithOpen=false"] : []
|
|
1090
|
+
];
|
|
1091
|
+
var dotnetWatchEnv = (devUrl) => ({
|
|
1092
|
+
VIDRA_DEV_URL: devUrl,
|
|
1093
|
+
DOTNET_WATCH_RESTART_ON_RUDE_EDIT: "1",
|
|
1094
|
+
DOTNET_WATCH_SUPPRESS_EMOJIS: "1",
|
|
1095
|
+
DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER: "1"
|
|
1096
|
+
});
|
|
1097
|
+
var HOST_READY_SENTINEL = "[vidra] host ready";
|
|
1098
|
+
var classifyWatchLine = (line) => {
|
|
1099
|
+
if (/waiting for a file to change/i.test(line)) return "appWaiting";
|
|
1100
|
+
if (/fix the error to continue/i.test(line)) return "buildBlocked";
|
|
1101
|
+
if (line.includes(HOST_READY_SENTINEL)) return "appStarted";
|
|
1102
|
+
if (/\bwatch\b/i.test(line) && /\b(?:started|launched)\b/i.test(line)) {
|
|
1103
|
+
return "appStarted";
|
|
1104
|
+
}
|
|
1105
|
+
return null;
|
|
1106
|
+
};
|
|
987
1107
|
var DevSession = class {
|
|
988
|
-
constructor(project, target, viteUrl, verbose, options
|
|
1108
|
+
constructor(project, target, viteUrl, verbose, options) {
|
|
989
1109
|
this.project = project;
|
|
990
1110
|
this.target = target;
|
|
991
1111
|
this.viteUrl = viteUrl;
|
|
992
1112
|
this.verbose = verbose;
|
|
993
|
-
this.vite = options.vite
|
|
1113
|
+
this.vite = options.vite;
|
|
1114
|
+
this.hotReload = options.hotReload;
|
|
994
1115
|
}
|
|
995
1116
|
project;
|
|
996
1117
|
target;
|
|
@@ -999,7 +1120,19 @@ var DevSession = class {
|
|
|
999
1120
|
children = [];
|
|
1000
1121
|
buildConfig = process.env.VIDRA_BUILD_CONFIG || "Debug";
|
|
1001
1122
|
vite;
|
|
1123
|
+
hotReload;
|
|
1002
1124
|
shuttingDown = false;
|
|
1125
|
+
// Watch-mode state: `watchChild` is the `dotnet watch` process (a process
|
|
1126
|
+
// group leader on unix, so shutdown can signal it and the app it launched
|
|
1127
|
+
// together); `watchReady` flips once the app has started at least once.
|
|
1128
|
+
watchChild;
|
|
1129
|
+
watchReady = false;
|
|
1130
|
+
watchOutputTail = "";
|
|
1131
|
+
endSession = () => {
|
|
1132
|
+
};
|
|
1133
|
+
sessionDone = new Promise((resolve) => {
|
|
1134
|
+
this.endSession = resolve;
|
|
1135
|
+
});
|
|
1003
1136
|
async run() {
|
|
1004
1137
|
this.installSignalHandlers();
|
|
1005
1138
|
console.log();
|
|
@@ -1007,9 +1140,8 @@ var DevSession = class {
|
|
|
1007
1140
|
console.log(kv("project", this.project.projectName));
|
|
1008
1141
|
console.log(kv("target", this.target.framework));
|
|
1009
1142
|
console.log();
|
|
1010
|
-
let vite;
|
|
1011
1143
|
if (this.vite) {
|
|
1012
|
-
|
|
1144
|
+
this.startVite();
|
|
1013
1145
|
try {
|
|
1014
1146
|
await waitForServer(this.viteUrl, POLL_TIMEOUT_MS);
|
|
1015
1147
|
} catch (error) {
|
|
@@ -1028,19 +1160,22 @@ var DevSession = class {
|
|
|
1028
1160
|
)
|
|
1029
1161
|
);
|
|
1030
1162
|
}
|
|
1031
|
-
|
|
1163
|
+
this.launchHost();
|
|
1164
|
+
const hostDirLabel = `src/${path6.basename(this.project.hostDir)}`;
|
|
1032
1165
|
if (this.vite) {
|
|
1033
1166
|
console.log(
|
|
1034
1167
|
taggedRow(
|
|
1035
1168
|
"active",
|
|
1036
1169
|
null,
|
|
1037
|
-
`${lime("hot reload active")} ${dim("\u2014 edit ui/src and save")}`
|
|
1170
|
+
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
1171
|
)
|
|
1039
1172
|
);
|
|
1040
1173
|
console.log();
|
|
1041
1174
|
console.log(
|
|
1042
1175
|
footer(
|
|
1043
|
-
`${dim("watching")} ${value("ui/")} ${dim(
|
|
1176
|
+
this.hotReload ? `${dim("watching")} ${value("ui/")} ${dim("\xB7")} ${value(`${hostDirLabel}/`)} ${dim(
|
|
1177
|
+
"\xB7 hot reload on save \xB7 ctrl-c to stop"
|
|
1178
|
+
)}` : `${dim("watching")} ${value("ui/")} ${dim(
|
|
1044
1179
|
"\xB7 hot reload on save \xB7 ctrl-c to stop"
|
|
1045
1180
|
)}`
|
|
1046
1181
|
)
|
|
@@ -1052,7 +1187,7 @@ var DevSession = class {
|
|
|
1052
1187
|
);
|
|
1053
1188
|
}
|
|
1054
1189
|
console.log();
|
|
1055
|
-
await
|
|
1190
|
+
await this.sessionDone;
|
|
1056
1191
|
}
|
|
1057
1192
|
installSignalHandlers() {
|
|
1058
1193
|
process.on("SIGINT", () => {
|
|
@@ -1072,6 +1207,166 @@ var DevSession = class {
|
|
|
1072
1207
|
});
|
|
1073
1208
|
return this.registerChild(vite, "ui", "Vite dev server");
|
|
1074
1209
|
}
|
|
1210
|
+
launchHost() {
|
|
1211
|
+
if (this.hotReload) {
|
|
1212
|
+
return this.launchHostWithWatch();
|
|
1213
|
+
}
|
|
1214
|
+
return this.target.name === "macos" ? this.launchMacosHost() : this.launchWindowsHost();
|
|
1215
|
+
}
|
|
1216
|
+
// --- dotnet watch launch (C# hot reload) -----------------------------------
|
|
1217
|
+
launchHostWithWatch() {
|
|
1218
|
+
console.log(
|
|
1219
|
+
taggedRow(
|
|
1220
|
+
"active",
|
|
1221
|
+
"host",
|
|
1222
|
+
`${dim("dotnet watch \u2014 building")} ${value(this.target.framework)} ${dim("\u2026")}`
|
|
1223
|
+
)
|
|
1224
|
+
);
|
|
1225
|
+
const watch = spawn2(
|
|
1226
|
+
DOTNET_COMMAND,
|
|
1227
|
+
buildDotnetWatchArgs({
|
|
1228
|
+
csprojPath: this.project.csprojPath,
|
|
1229
|
+
framework: this.target.framework,
|
|
1230
|
+
buildConfig: this.buildConfig,
|
|
1231
|
+
verbose: this.verbose,
|
|
1232
|
+
targetName: this.target.name
|
|
1233
|
+
}),
|
|
1234
|
+
{
|
|
1235
|
+
cwd: this.project.root,
|
|
1236
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1237
|
+
env: { ...process.env, ...dotnetWatchEnv(this.viteUrl) },
|
|
1238
|
+
// Unix: make dotnet watch its own process-group leader so shutdown can
|
|
1239
|
+
// SIGTERM the group — taking down both watch and the app it launched.
|
|
1240
|
+
// Windows cleanup goes through `taskkill /T` instead.
|
|
1241
|
+
detached: process.platform !== "win32"
|
|
1242
|
+
}
|
|
1243
|
+
);
|
|
1244
|
+
this.watchChild = watch;
|
|
1245
|
+
this.children.push(watch);
|
|
1246
|
+
const notSentinel = (line) => !line.includes(HOST_READY_SENTINEL);
|
|
1247
|
+
prefixStream(watch.stdout, "host", notSentinel);
|
|
1248
|
+
prefixStream(watch.stderr, "host", notSentinel);
|
|
1249
|
+
scanStream(watch.stdout, (line) => this.onWatchLine(line));
|
|
1250
|
+
scanStream(watch.stderr, (line) => this.onWatchLine(line));
|
|
1251
|
+
watch.on("exit", (code, signal) => {
|
|
1252
|
+
if (this.shuttingDown) return;
|
|
1253
|
+
if (this.watchReady) {
|
|
1254
|
+
const failed = code !== null && code !== 0 || signal !== null;
|
|
1255
|
+
if (failed) {
|
|
1256
|
+
console.error(
|
|
1257
|
+
"\n" + row({
|
|
1258
|
+
glyph: "error",
|
|
1259
|
+
detail: dim(
|
|
1260
|
+
`dotnet watch exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
1261
|
+
)
|
|
1262
|
+
})
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
this.shutdown(code ?? (signal ? 1 : 0));
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.fallBackToClassicLaunch(code, signal);
|
|
1269
|
+
});
|
|
1270
|
+
watch.on("error", (error) => {
|
|
1271
|
+
if (this.shuttingDown) return;
|
|
1272
|
+
console.error(
|
|
1273
|
+
"\n" + row({
|
|
1274
|
+
glyph: "error",
|
|
1275
|
+
detail: dim(`failed to start dotnet watch: ${error.message}`)
|
|
1276
|
+
})
|
|
1277
|
+
);
|
|
1278
|
+
this.shutdown(1);
|
|
1279
|
+
});
|
|
1280
|
+
return watch;
|
|
1281
|
+
}
|
|
1282
|
+
onWatchLine(line) {
|
|
1283
|
+
this.watchOutputTail = (this.watchOutputTail + line + "\n").slice(
|
|
1284
|
+
-WATCH_OUTPUT_TAIL_CHARS
|
|
1285
|
+
);
|
|
1286
|
+
const event = classifyWatchLine(line);
|
|
1287
|
+
if (event === "appStarted" && !this.watchReady) {
|
|
1288
|
+
this.watchReady = true;
|
|
1289
|
+
console.log(
|
|
1290
|
+
taggedRow(
|
|
1291
|
+
"done",
|
|
1292
|
+
"host",
|
|
1293
|
+
`${dim("launched")} ${value(this.project.projectName)} ${dim("\u2014 C# hot reload active")}`
|
|
1294
|
+
)
|
|
1295
|
+
);
|
|
1296
|
+
} else if (event === "appWaiting" && !this.shuttingDown) {
|
|
1297
|
+
console.log(
|
|
1298
|
+
taggedRow(
|
|
1299
|
+
"manual",
|
|
1300
|
+
"host",
|
|
1301
|
+
dim("app not running \u2014 save a C# file to relaunch, or ctrl-c to stop")
|
|
1302
|
+
)
|
|
1303
|
+
);
|
|
1304
|
+
} else if (event === "buildBlocked" && !this.shuttingDown) {
|
|
1305
|
+
if (!this.watchReady) {
|
|
1306
|
+
if (looksLikeMissingWorkload(this.watchOutputTail)) printWorkloadHint();
|
|
1307
|
+
else if (looksLikeXcodeTooOld(this.watchOutputTail)) printXcodeTooOldHint();
|
|
1308
|
+
else if (looksLikeMissingXcode(this.watchOutputTail)) printXcodeHint();
|
|
1309
|
+
}
|
|
1310
|
+
console.log(
|
|
1311
|
+
taggedRow(
|
|
1312
|
+
"manual",
|
|
1313
|
+
"host",
|
|
1314
|
+
dim("build failed \u2014 fix the error and save to retry, or ctrl-c to stop")
|
|
1315
|
+
)
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* `dotnet watch` exited before the app ever started — typically an SDK or
|
|
1321
|
+
* MAUI workload set that predates `dotnet run`/`dotnet watch` support for
|
|
1322
|
+
* this target. Explain why, then launch the host the classic way (one
|
|
1323
|
+
* build + direct spawn) so the dev session still works, minus C# hot reload.
|
|
1324
|
+
*/
|
|
1325
|
+
fallBackToClassicLaunch(code, signal) {
|
|
1326
|
+
console.log();
|
|
1327
|
+
console.log(
|
|
1328
|
+
taggedRow(
|
|
1329
|
+
"manual",
|
|
1330
|
+
"host",
|
|
1331
|
+
`${dim("C# hot reload unavailable \u2014 dotnet watch exited with")} ${value(
|
|
1332
|
+
signal ? `signal ${signal}` : `code ${code ?? "unknown"}`
|
|
1333
|
+
)}`
|
|
1334
|
+
)
|
|
1335
|
+
);
|
|
1336
|
+
if (looksLikeMissingWorkload(this.watchOutputTail)) {
|
|
1337
|
+
printWorkloadHint();
|
|
1338
|
+
} else if (looksLikeXcodeTooOld(this.watchOutputTail)) {
|
|
1339
|
+
printXcodeTooOldHint();
|
|
1340
|
+
} else if (looksLikeMissingXcode(this.watchOutputTail)) {
|
|
1341
|
+
printXcodeHint();
|
|
1342
|
+
} else {
|
|
1343
|
+
console.log(
|
|
1344
|
+
footer(
|
|
1345
|
+
dim(
|
|
1346
|
+
"C# hot reload needs the .NET 10.0.203+ workload set (Mac Catalyst dotnet-watch support) \u2014 try:"
|
|
1347
|
+
)
|
|
1348
|
+
)
|
|
1349
|
+
);
|
|
1350
|
+
console.log(fixLine("dotnet workload update"));
|
|
1351
|
+
if (!this.verbose) {
|
|
1352
|
+
console.log(footer(dim("re-run with --verbose for the full watch log.")));
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
console.log(
|
|
1356
|
+
taggedRow(
|
|
1357
|
+
"active",
|
|
1358
|
+
"host",
|
|
1359
|
+
dim("falling back to a classic launch (frontend hot reload still active)\u2026")
|
|
1360
|
+
)
|
|
1361
|
+
);
|
|
1362
|
+
console.log();
|
|
1363
|
+
if (this.target.name === "macos") {
|
|
1364
|
+
this.launchMacosHost();
|
|
1365
|
+
} else {
|
|
1366
|
+
this.launchWindowsHost();
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// --- classic launch (one build + direct spawn) ------------------------------
|
|
1075
1370
|
// Builds the MAUI host as a discrete step (a plain `dotnet build`, never
|
|
1076
1371
|
// MSBuild's `-t:Run`) so the per-OS launch paths can spawn the produced
|
|
1077
1372
|
// binary directly. `-t:Run` shells out in ways that break for both locally
|
|
@@ -1194,8 +1489,9 @@ var DevSession = class {
|
|
|
1194
1489
|
}
|
|
1195
1490
|
registerChild(child, tag2, label) {
|
|
1196
1491
|
this.children.push(child);
|
|
1197
|
-
|
|
1198
|
-
prefixStream(child.
|
|
1492
|
+
const include = tag2 === "host" ? (line) => !line.includes(HOST_READY_SENTINEL) : void 0;
|
|
1493
|
+
prefixStream(child.stdout, tag2, include);
|
|
1494
|
+
prefixStream(child.stderr, tag2, include);
|
|
1199
1495
|
child.on("exit", (code, signal) => {
|
|
1200
1496
|
if (this.shuttingDown) return;
|
|
1201
1497
|
if (tag2 === "ui") {
|
|
@@ -1238,8 +1534,9 @@ var DevSession = class {
|
|
|
1238
1534
|
if (this.shuttingDown) return;
|
|
1239
1535
|
this.shuttingDown = true;
|
|
1240
1536
|
for (const child of this.children) {
|
|
1241
|
-
killChild(child);
|
|
1537
|
+
killChild(child, { processGroup: child === this.watchChild });
|
|
1242
1538
|
}
|
|
1539
|
+
this.endSession();
|
|
1243
1540
|
process.exit(exitCode);
|
|
1244
1541
|
}
|
|
1245
1542
|
};
|
|
@@ -1257,19 +1554,32 @@ var ensureTargetMatchesHostOs = (targetName) => {
|
|
|
1257
1554
|
process.exit(1);
|
|
1258
1555
|
}
|
|
1259
1556
|
};
|
|
1260
|
-
var prefixStream = (stream, tag2) => {
|
|
1557
|
+
var prefixStream = (stream, tag2, include = () => true) => {
|
|
1261
1558
|
if (!stream) return;
|
|
1262
1559
|
const prefix = streamPrefix(tag2);
|
|
1263
1560
|
stream.on("data", (chunk) => {
|
|
1264
1561
|
const lines = chunk.toString().split("\n");
|
|
1265
1562
|
for (const line of lines) {
|
|
1266
|
-
if (line.length > 0) {
|
|
1563
|
+
if (line.length > 0 && include(line)) {
|
|
1267
1564
|
process.stdout.write(`${prefix} ${line}
|
|
1268
1565
|
`);
|
|
1269
1566
|
}
|
|
1270
1567
|
}
|
|
1271
1568
|
});
|
|
1272
1569
|
};
|
|
1570
|
+
var scanStream = (stream, onLine) => {
|
|
1571
|
+
if (!stream) return;
|
|
1572
|
+
let pending = "";
|
|
1573
|
+
stream.on("data", (chunk) => {
|
|
1574
|
+
pending += chunk.toString();
|
|
1575
|
+
const lines = pending.split("\n");
|
|
1576
|
+
pending = lines.pop() ?? "";
|
|
1577
|
+
for (const line of lines) onLine(line);
|
|
1578
|
+
});
|
|
1579
|
+
stream.on("end", () => {
|
|
1580
|
+
if (pending) onLine(pending);
|
|
1581
|
+
});
|
|
1582
|
+
};
|
|
1273
1583
|
var waitForServer = (url, timeoutMs) => {
|
|
1274
1584
|
const { hostname, port, pathname } = new URL(url);
|
|
1275
1585
|
const start = Date.now();
|
|
@@ -1301,14 +1611,6 @@ var waitForServer = (url, timeoutMs) => {
|
|
|
1301
1611
|
poll();
|
|
1302
1612
|
});
|
|
1303
1613
|
};
|
|
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
1614
|
var findMacAppBundle = (hostDir, framework, buildConfig) => {
|
|
1313
1615
|
const outputDir = path6.join(hostDir, "bin", buildConfig, framework);
|
|
1314
1616
|
return findAppBundleRecursive(outputDir);
|
|
@@ -1362,7 +1664,7 @@ var findFileRecursive = (dir, match) => {
|
|
|
1362
1664
|
}
|
|
1363
1665
|
return null;
|
|
1364
1666
|
};
|
|
1365
|
-
var killChild = (child) => {
|
|
1667
|
+
var killChild = (child, opts = {}) => {
|
|
1366
1668
|
if (!child.pid || child.exitCode !== null) return;
|
|
1367
1669
|
if (process.platform === "win32") {
|
|
1368
1670
|
try {
|
|
@@ -1374,6 +1676,13 @@ var killChild = (child) => {
|
|
|
1374
1676
|
}
|
|
1375
1677
|
return;
|
|
1376
1678
|
}
|
|
1679
|
+
if (opts.processGroup) {
|
|
1680
|
+
try {
|
|
1681
|
+
process.kill(-child.pid, "SIGTERM");
|
|
1682
|
+
return;
|
|
1683
|
+
} catch {
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1377
1686
|
child.kill("SIGTERM");
|
|
1378
1687
|
};
|
|
1379
1688
|
var printMacLaunchHint = () => {
|
|
@@ -1411,7 +1720,7 @@ var printHelp = () => {
|
|
|
1411
1720
|
${lime("vidra")} ${dim("<command> [options]")}
|
|
1412
1721
|
|
|
1413
1722
|
${dim("commands")}
|
|
1414
|
-
${cmd("dev", "start vite + the native host (hot reload)")}
|
|
1723
|
+
${cmd("dev", "start vite + the native host (UI + C# hot reload)")}
|
|
1415
1724
|
${cmd("run", "launch the native host only")}
|
|
1416
1725
|
${cmd("build", "build & package for distribution")}
|
|
1417
1726
|
${cmd("doctor", "check your environment")}
|
|
@@ -1419,6 +1728,7 @@ ${cmd("help", "show this message")}
|
|
|
1419
1728
|
|
|
1420
1729
|
${dim("examples")}
|
|
1421
1730
|
${ex("dev --target windows", "run the windows host")}
|
|
1731
|
+
${ex("dev --no-hot-reload", "skip dotnet watch, classic launch")}
|
|
1422
1732
|
${ex("build --plan", "preview the build, run nothing")}
|
|
1423
1733
|
${ex("build --target macos", "build & package a macOS DMG")}
|
|
1424
1734
|
${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.1";
|
|
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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { app, appWindow, clipboard, notifications, vidra } from "@vidra-dev/sdk";
|
|
1
|
+
import { type MouseEvent, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { app, appWindow, browser, clipboard, notifications, vidra } from "@vidra-dev/sdk";
|
|
3
3
|
import type { WindowInfo, WindowSupport } from "@vidra-dev/sdk";
|
|
4
4
|
|
|
5
5
|
const describeWindow = (windowInfo: WindowInfo): string => {
|
|
@@ -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
|
|
|
@@ -172,6 +190,18 @@ const App = () => {
|
|
|
172
190
|
}
|
|
173
191
|
};
|
|
174
192
|
|
|
193
|
+
const handleOpenVidraSite = async (
|
|
194
|
+
event: MouseEvent<HTMLAnchorElement>,
|
|
195
|
+
) => {
|
|
196
|
+
// Open in the user's default browser instead of navigating the webview.
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
try {
|
|
199
|
+
await browser.open({ url: "https://vidra.build", mode: "external" });
|
|
200
|
+
} catch (error: unknown) {
|
|
201
|
+
setInfo(`Error: ${formatError(error)}`);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
175
205
|
const showsAdvancedWindowActions =
|
|
176
206
|
!!windowSupport &&
|
|
177
207
|
(windowSupport.center ||
|
|
@@ -188,6 +218,12 @@ const App = () => {
|
|
|
188
218
|
<span className="dot" />
|
|
189
219
|
React + .NET MAUI
|
|
190
220
|
</span>
|
|
221
|
+
{csReloaded && (
|
|
222
|
+
<span className="badge reload-badge">
|
|
223
|
+
<span className="dot" />
|
|
224
|
+
{csReloaded}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
191
227
|
<h1 className="title">{{appTitle}}</h1>
|
|
192
228
|
<p className="tagline">
|
|
193
229
|
A cross-platform desktop app with a native bridge.
|
|
@@ -254,9 +290,10 @@ const App = () => {
|
|
|
254
290
|
<footer className="footer">
|
|
255
291
|
Built with{" "}
|
|
256
292
|
<a
|
|
257
|
-
href="https://
|
|
293
|
+
href="https://vidra.dev"
|
|
258
294
|
target="_blank"
|
|
259
295
|
rel="noreferrer"
|
|
296
|
+
onClick={handleOpenVidraSite}
|
|
260
297
|
>
|
|
261
298
|
Vidra
|
|
262
299
|
</a>
|
|
@@ -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);
|