create-vidra-app 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ import { execSync, spawn } from "child_process";
33
33
 
34
34
  // src/theme.ts
35
35
  import chalk from "chalk";
36
- var CLI_VERSION = "0.1.5";
36
+ var CLI_VERSION = "0.3.0";
37
37
  var STEP_LABEL_WIDTH = 16;
38
38
  var lime = chalk.hex("#c8f751");
39
39
  var value = chalk.hex("#e8e8ec");
@@ -400,6 +400,7 @@ var newestNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).map((line) =
400
400
  var outputMentionsMaui = (workloadListOutput) => MAUI_WORKLOAD.test(workloadListOutput);
401
401
  var workloadSetVersion = (workloadListOutput) => workloadListOutput.match(WORKLOAD_SET_VERSION_LINE)?.[1];
402
402
  var MIN_MACCATALYST_WATCH_WORKLOAD_SET = [10, 0, 203];
403
+ var MIN_MACCATALYST_WATCH_SDK = [10, 0, 300];
403
404
  var meetsMinimumVersion = (version, minimum) => {
404
405
  const segments = version.split(".").map((s) => Number.parseInt(s, 10));
405
406
  if (segments.slice(0, minimum.length).some(Number.isNaN)) return false;
@@ -410,12 +411,25 @@ var meetsMinimumVersion = (version, minimum) => {
410
411
  return true;
411
412
  };
412
413
  var workloadSetSupportsCSharpHotReload = (version) => meetsMinimumVersion(version, MIN_MACCATALYST_WATCH_WORKLOAD_SET);
414
+ var sdkSupportsCSharpHotReload = (version) => meetsMinimumVersion(version, MIN_MACCATALYST_WATCH_SDK);
413
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
+ }
414
425
  const res = run(DOTNET, ["workload", "list"]);
415
426
  if (!res.found) return null;
416
427
  const version = workloadSetVersion(res.stdout);
417
- if (!version) return null;
418
- return workloadSetSupportsCSharpHotReload(version) ? null : version;
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
+ };
419
433
  };
420
434
  var MISSING_WORKLOAD_SIGNATURES = [
421
435
  /NETSDK1147/i,
@@ -487,6 +501,15 @@ var checkCSharpHotReload = (workloadList) => {
487
501
  if (process.platform !== "darwin") {
488
502
  return { name, status: "ok", detail: "dotnet watch supported" };
489
503
  }
504
+ const sdkVersion = run(DOTNET, ["--version"]).stdout.trim();
505
+ if (sdkVersion && !sdkSupportsCSharpHotReload(sdkVersion)) {
506
+ return {
507
+ name,
508
+ status: "unknown",
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
511
+ };
512
+ }
490
513
  const version = workloadSetVersion(workloadList.stdout);
491
514
  if (!version) {
492
515
  return {
@@ -1011,6 +1034,83 @@ import path6 from "path";
1011
1034
  import fs5 from "fs-extra";
1012
1035
  import { execFileSync as execFileSync4, spawn as spawn2 } from "child_process";
1013
1036
  import { request } from "http";
1037
+
1038
+ // src/dev-port.ts
1039
+ import { createServer } from "net";
1040
+ import prompts2 from "prompts";
1041
+ var MIN_PORT = 1;
1042
+ var MAX_PORT = 65535;
1043
+ var isValidPort = (port) => Number.isInteger(port) && port >= MIN_PORT && port <= MAX_PORT;
1044
+ var isPortAvailable = (port, hostname) => new Promise((resolve, reject) => {
1045
+ const server = createServer();
1046
+ server.unref();
1047
+ server.once("error", (error) => {
1048
+ if (error.code === "EADDRINUSE" || error.code === "EACCES") {
1049
+ resolve(false);
1050
+ return;
1051
+ }
1052
+ reject(error);
1053
+ });
1054
+ server.listen({ host: hostname, port, exclusive: true }, () => {
1055
+ server.close((error) => {
1056
+ if (error) {
1057
+ reject(error);
1058
+ return;
1059
+ }
1060
+ resolve(true);
1061
+ });
1062
+ });
1063
+ });
1064
+ var findNextAvailablePort = async (occupiedPort, hostname, probe = isPortAvailable) => {
1065
+ for (let port = occupiedPort + 1; port <= MAX_PORT; port++) {
1066
+ if (await probe(port, hostname)) return port;
1067
+ }
1068
+ throw new Error(`No available port found after ${occupiedPort}`);
1069
+ };
1070
+ var defaultPrompt = async (question) => {
1071
+ const result = await prompts2(question, { onCancel: () => false });
1072
+ return { port: result.port };
1073
+ };
1074
+ var selectDevServerUrl = async (configuredUrl, options = {}) => {
1075
+ const url = new URL(configuredUrl);
1076
+ const port = Number(url.port);
1077
+ if (!isValidPort(port)) {
1078
+ throw new Error(`Dev server URL must include a valid port: ${configuredUrl}`);
1079
+ }
1080
+ const probe = options.isPortAvailable ?? isPortAvailable;
1081
+ if (await probe(port, url.hostname)) return configuredUrl;
1082
+ const interactive = options.interactive ?? isInteractive();
1083
+ if (!interactive) {
1084
+ throw new Error(
1085
+ `Port ${port} is already in use and no interactive terminal is available`
1086
+ );
1087
+ }
1088
+ const suggestedPort = await findNextAvailablePort(port, url.hostname, probe);
1089
+ const prompt = options.prompt ?? defaultPrompt;
1090
+ const result = await prompt({
1091
+ type: "number",
1092
+ name: "port",
1093
+ message: `Port ${port} is already in use. Choose another port:`,
1094
+ initial: suggestedPort,
1095
+ validate: async (value2) => {
1096
+ if (!isValidPort(value2)) {
1097
+ return `Enter a port between ${MIN_PORT} and ${MAX_PORT}`;
1098
+ }
1099
+ return await probe(value2, url.hostname) || `Port ${value2} is already in use`;
1100
+ }
1101
+ });
1102
+ const selectedPort = result.port;
1103
+ if (selectedPort === void 0) {
1104
+ throw new Error("Port selection cancelled");
1105
+ }
1106
+ if (!isValidPort(selectedPort) || !await probe(selectedPort, url.hostname)) {
1107
+ throw new Error(`Port ${selectedPort} is not available`);
1108
+ }
1109
+ url.port = String(selectedPort);
1110
+ return url.toString();
1111
+ };
1112
+
1113
+ // src/commands/dev.ts
1014
1114
  var POLL_INTERVAL_MS = 500;
1015
1115
  var POLL_TIMEOUT_MS = 3e4;
1016
1116
  var NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm";
@@ -1033,7 +1133,7 @@ var startSession = async (argv, opts) => {
1033
1133
  const targetName = args["target"] || detectPlatform();
1034
1134
  const verbose = !!args["verbose"];
1035
1135
  let hotReload = opts.hotReloadDefault && !args["no-hot-reload"];
1036
- const viteUrl = process.env.VIDRA_DEV_URL || "http://localhost:5173";
1136
+ let viteUrl = process.env.VIDRA_DEV_URL || "http://localhost:5173";
1037
1137
  const target = TARGETS2[targetName];
1038
1138
  if (!target) {
1039
1139
  const supported = Object.keys(TARGETS2).join(", ");
@@ -1058,12 +1158,15 @@ var startSession = async (argv, opts) => {
1058
1158
  console.log(
1059
1159
  row({
1060
1160
  glyph: "manual",
1061
- detail: `${dim("C# hot reload needs workload set 10.0.203+ \u2014 found")} ${value(blocker)}${dim("; using a classic launch")}`
1161
+ detail: `${dim(blocker.reason + "; using a classic launch")}`
1062
1162
  })
1063
1163
  );
1064
- console.log(fixLine("dotnet workload update"));
1164
+ console.log(fixLine(blocker.fix));
1065
1165
  }
1066
1166
  }
1167
+ if (opts.vite) {
1168
+ viteUrl = await selectDevServerUrl(viteUrl);
1169
+ }
1067
1170
  const session = new DevSession(project, target, viteUrl, verbose, {
1068
1171
  vite: opts.vite,
1069
1172
  hotReload
@@ -1094,6 +1197,14 @@ var dotnetWatchEnv = (devUrl) => ({
1094
1197
  DOTNET_WATCH_SUPPRESS_EMOJIS: "1",
1095
1198
  DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER: "1"
1096
1199
  });
1200
+ var buildViteArgs = (devUrl) => [
1201
+ "run",
1202
+ "dev",
1203
+ "--",
1204
+ "--port",
1205
+ new URL(devUrl).port,
1206
+ "--strictPort"
1207
+ ];
1097
1208
  var HOST_READY_SENTINEL = "[vidra] host ready";
1098
1209
  var classifyWatchLine = (line) => {
1099
1210
  if (/waiting for a file to change/i.test(line)) return "appWaiting";
@@ -1200,7 +1311,7 @@ var DevSession = class {
1200
1311
  }
1201
1312
  startVite() {
1202
1313
  console.log(taggedRow("active", "ui", dim("starting dev server\u2026")));
1203
- const vite = spawn2(NPM_COMMAND, ["run", "dev"], {
1314
+ const vite = spawn2(NPM_COMMAND, buildViteArgs(this.viteUrl), {
1204
1315
  cwd: this.project.uiDir,
1205
1316
  stdio: ["ignore", "pipe", "pipe"],
1206
1317
  shell: process.platform === "win32"
@@ -1293,7 +1404,12 @@ var DevSession = class {
1293
1404
  `${dim("launched")} ${value(this.project.projectName)} ${dim("\u2014 C# hot reload active")}`
1294
1405
  )
1295
1406
  );
1296
- } else if (event === "appWaiting" && !this.shuttingDown) {
1407
+ return;
1408
+ }
1409
+ if (event !== "appWaiting" && event !== "buildBlocked" || this.shuttingDown) {
1410
+ return;
1411
+ }
1412
+ if (this.watchReady) {
1297
1413
  console.log(
1298
1414
  taggedRow(
1299
1415
  "manual",
@@ -1301,20 +1417,30 @@ var DevSession = class {
1301
1417
  dim("app not running \u2014 save a C# file to relaunch, or ctrl-c to stop")
1302
1418
  )
1303
1419
  );
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
- }
1420
+ return;
1421
+ }
1422
+ if (!/Build FAILED/i.test(this.watchOutputTail)) {
1310
1423
  console.log(
1311
1424
  taggedRow(
1312
1425
  "manual",
1313
1426
  "host",
1314
- dim("build failed \u2014 fix the error and save to retry, or ctrl-c to stop")
1427
+ dim(
1428
+ "app exited before it was ready \u2014 save a C# file to relaunch, or ctrl-c to stop"
1429
+ )
1315
1430
  )
1316
1431
  );
1432
+ return;
1317
1433
  }
1434
+ if (looksLikeMissingWorkload(this.watchOutputTail)) printWorkloadHint();
1435
+ else if (looksLikeXcodeTooOld(this.watchOutputTail)) printXcodeTooOldHint();
1436
+ else if (looksLikeMissingXcode(this.watchOutputTail)) printXcodeHint();
1437
+ console.log(
1438
+ taggedRow(
1439
+ "manual",
1440
+ "host",
1441
+ dim("build failed \u2014 fix the error and save to retry, or ctrl-c to stop")
1442
+ )
1443
+ );
1318
1444
  }
1319
1445
  /**
1320
1446
  * `dotnet watch` exited before the app ever started — typically an SDK or
package/dist/index.js CHANGED
@@ -329,8 +329,8 @@ var VIDRA_REPO_ROOT = path2.resolve(CLI_ROOT, "..", "..", "..");
329
329
  var LOCAL_FEED_DIR = path2.join(VIDRA_REPO_ROOT, "dist", "packages");
330
330
  var LOCAL_CLI_DIR = CLI_ROOT;
331
331
  var LOCAL_SDK_DIR = path2.join(VIDRA_REPO_ROOT, "src", "sdk", "vidra-js");
332
- var VIDRA_VERSION = "0.2.1";
333
- var SDK_VERSION = "0.1.0";
332
+ var VIDRA_VERSION = "0.3.0";
333
+ var SDK_VERSION = "0.2.0";
334
334
  var main = async () => {
335
335
  console.log();
336
336
  console.log(` create-${lime("vidra")}-app`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vidra-app",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold a new Vidra application (React + .NET MAUI)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,10 @@
1
+ using Vidra.Bridge;
2
+
3
+ namespace {{projectName}};
4
+
5
+ [JsContract("counter")]
6
+ public interface ICounterJs
7
+ {
8
+ [JsMethod("increment")]
9
+ Task<int> IncrementAsync();
10
+ }
@@ -22,7 +22,7 @@ public class MainPage : VidraPage
22
22
  {
23
23
  try
24
24
  {
25
- var count = await Bridge.CallJsAsync<int>("counter.increment");
25
+ var count = await Bridge.Js().Counter.IncrementAsync();
26
26
  System.Diagnostics.Debug.WriteLine($"[MainPage] Counter is now {count}");
27
27
  }
28
28
  catch (Exception ex)
@@ -42,6 +42,7 @@
42
42
  preserved.
43
43
  -->
44
44
  <MtouchLink Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst' And '$(Configuration)' == 'Debug'">None</MtouchLink>
45
+ <VidraTsOutputDir>$(MSBuildProjectDirectory)/../../ui/src/generated</VidraTsOutputDir>
45
46
  </PropertyGroup>
46
47
 
47
48
  <ItemGroup>
@@ -1,6 +1,15 @@
1
1
  import { type MouseEvent, useEffect, useRef, useState } from "react";
2
- import { app, appWindow, browser, clipboard, notifications, vidra } from "@vidra-dev/sdk";
3
- import type { WindowInfo, WindowSupport } from "@vidra-dev/sdk";
2
+ import {
3
+ app,
4
+ appWindow,
5
+ browser,
6
+ clipboard,
7
+ notifications,
8
+ runtime,
9
+ vidra,
10
+ } from "@vidra-dev/sdk";
11
+ import type { Capabilities, WindowInfo, WindowSupport } from "@vidra-dev/sdk";
12
+ import { counterHandlers } from "./generated/index.js";
4
13
 
5
14
  const describeWindow = (windowInfo: WindowInfo): string => {
6
15
  const title = windowInfo.title || "(untitled)";
@@ -17,22 +26,21 @@ const App = () => {
17
26
  "Window controls are ready. Click a button to inspect or update the native window.",
18
27
  );
19
28
  const [windowSupport, setWindowSupport] = useState<WindowSupport | null>(null);
20
- const [caps, setCaps] = useState<Record<string, string[]> | null>(null);
29
+ const [caps, setCaps] = useState<Capabilities | null>(null);
21
30
  const [count, setCount] = useState(0);
22
31
  const [csReloaded, setCsReloaded] = useState<string | null>(null);
23
32
  const countRef = useRef(0);
24
33
  const reloadFlashTimer = useRef<number | undefined>(undefined);
25
34
 
26
35
  useEffect(() => {
27
- const unsubscribeCounter = vidra.handle<void, number>("counter.increment", () => {
36
+ const unsubscribeCounter = counterHandlers.increment(() => {
28
37
  countRef.current += 1;
29
38
  setCount(countRef.current);
30
39
  return countRef.current;
31
40
  });
32
41
 
33
42
  // Emitted by the native host when `vidra dev` hot reloads edited C#.
34
- const unsubscribeHotReloaded = vidra.on<{ updatedTypes?: string[] }>(
35
- "vidra.hotReloaded",
43
+ const unsubscribeHotReloaded = runtime.onHotReloaded(
36
44
  (data) => {
37
45
  const typeName = data?.updatedTypes?.[0]?.split(".").pop();
38
46
  setCsReloaded(typeName ? `C# reloaded · ${typeName}` : "C# reloaded");
@@ -0,0 +1,10 @@
1
+ // Auto-generated by vidra-codegen. Do not edit.
2
+ import type { VidraClient } from "@vidra-dev/sdk";
3
+
4
+ export class CounterHandlers {
5
+ constructor(private client: VidraClient) {}
6
+
7
+ increment(handler: () => number | Promise<number>): () => void {
8
+ return this.client.unsafe.handle("counter", "increment", handler);
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ // Auto-generated by vidra-codegen. Do not edit.
2
+ import { vidra } from "@vidra-dev/sdk";
3
+ import { CounterHandlers } from "./counter.js";
4
+
5
+ vidra.registerGeneratedManifest("app", "d3044812d17c42049962d730fced04a6ecd711e287cdd0da7b218b327ee6cd56");
6
+
7
+ export const counterHandlers = new CounterHandlers(vidra);
8
+
9
+ export * from "./counter.js";
@@ -0,0 +1,17 @@
1
+ {
2
+ "contracts": {
3
+ "counter": {
4
+ "nativeMethods": {},
5
+ "events": {},
6
+ "jsMethods": {
7
+ "increment": {
8
+ "returns": {
9
+ "kind": "primitive",
10
+ "tsType": "number"
11
+ }
12
+ }
13
+ }
14
+ }
15
+ },
16
+ "fingerprint": "d3044812d17c42049962d730fced04a6ecd711e287cdd0da7b218b327ee6cd56"
17
+ }