expo-desktop 0.1.33 → 0.1.35

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.
@@ -26,6 +26,7 @@ export const AppJson = type({
26
26
  export const PackageJson = type({
27
27
  "name?": "string",
28
28
  "scripts?": "Record<string, string>",
29
+ "expo?": "Record<string, unknown.any>",
29
30
  "dependencies?": "Record<string, string>",
30
31
  "devDependencies?": "Record<string, string>",
31
32
  "peerDependencies?": "Record<string, string>",
@@ -0,0 +1,43 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ /** True when the project root has its own `.git` (from create-expo-app's init). */
5
+ export async function hasProjectGitRepositoryAsync(projectPath) {
6
+ try {
7
+ await fs.lstat(path.join(projectPath, ".git"));
8
+ return true;
9
+ }
10
+ catch (error) {
11
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
12
+ return false;
13
+ }
14
+ throw error;
15
+ }
16
+ }
17
+ /** `git diff --cached --quiet` exits 1 when there are staged changes. */
18
+ export async function hasGitStagedChangesAsync(projectPath) {
19
+ try {
20
+ await runGitAsync(projectPath, ["diff", "--cached", "--quiet"]);
21
+ return false;
22
+ }
23
+ catch {
24
+ return true;
25
+ }
26
+ }
27
+ function runGitAsync(projectPath, args) {
28
+ return new Promise((resolve, reject) => {
29
+ const cp = spawn(`git ${args.join(" ")}`, {
30
+ cwd: projectPath,
31
+ shell: true,
32
+ stdio: "ignore",
33
+ });
34
+ cp.on("error", reject);
35
+ cp.on("close", (code) => {
36
+ if (code === 0) {
37
+ resolve();
38
+ return;
39
+ }
40
+ reject(new Error(`git ${args.join(" ")} exited with code ${code ?? "null"}`));
41
+ });
42
+ });
43
+ }
@@ -135,6 +135,9 @@ export function packageManagerExec(packageManager) {
135
135
  command = "npx";
136
136
  args.push("--yes");
137
137
  break;
138
+ case "yarn":
139
+ command = "yarn";
140
+ break;
138
141
  case "pnpm":
139
142
  command = "pnpm";
140
143
  args.push("exec");
@@ -51,6 +51,7 @@ export async function newExpoDesktopProject(args) {
51
51
  },
52
52
  { value: "bun", label: `Bun${platform === "darwin" ? " (recommended)" : ""}` },
53
53
  { value: "pnpm", label: `pnpm${platform === "win32" ? " (recommended)" : ""}` },
54
+ { value: "yarn", label: "yarn" },
54
55
  ],
55
56
  initialValue: platform === "darwin" ? "bun" : platform === "win32" ? "pnpm" : "npm",
56
57
  });
@@ -11,6 +11,7 @@ import { applyConfigPlugins } from "../common/apply-config-plugins.js";
11
11
  import { makePrettySummary } from "../common/arktype.js";
12
12
  import { promisifiedSpawnTask, SPAWN_DEBUG_LOG_GLOB } from "../common/child-process.js";
13
13
  import { title } from "../common/clack.js";
14
+ import { hasGitStagedChangesAsync, hasProjectGitRepositoryAsync } from "../common/git.js";
14
15
  import { packageManagerExec } from "../common/npm.js";
15
16
  import { preserveFile } from "../common/preserve-file.js";
16
17
  import { applySelectedTemplatesAsync } from "../common/template.js";
@@ -103,6 +104,11 @@ export async function createExpoDesktopApp({ localDev, name, packageManager, tem
103
104
  await improveMetroConfig({ projectPath });
104
105
  title("Adding Expo support to the Babel config…", { spacing: 1 });
105
106
  await writeBabelConfig({ projectPath });
107
+ title("Improving App.tsx…", { spacing: 1 });
108
+ await improveAppTsx({ projectPath });
109
+ title("Committing changes…", { spacing: 1 });
110
+ await commitChanges({ projectPath });
111
+ logProjectReady({ cdPath: name.filesafeName, packageManager });
106
112
  }
107
113
  async function getBundleEntryFileCandidates({ projectPath }) {
108
114
  try {
@@ -148,8 +154,13 @@ async function createExpoApp({ localDev, name, packageManager, versions, }) {
148
154
  // create-expo-app for non-interactive template selection; set `CI=true` in the
149
155
  // child env so git-init inside an existing repo is skipped too.
150
156
  const command = packageManager === "npm" ? "npx" : packageManager;
157
+ let packageManagerArgs = ["create", "expo-app@latest"];
158
+ if (packageManager === "npm")
159
+ packageManagerArgs = ["--yes", "create-expo-app@latest"];
160
+ if (packageManager === "yarn")
161
+ packageManagerArgs = ["create", "expo-app"];
151
162
  const args = [
152
- ...(packageManager === "npm" ? ["--yes", "create-expo-app"] : ["create", "expo-app"]),
163
+ ...packageManagerArgs,
153
164
  name.filesafeName,
154
165
  "--yes",
155
166
  "--template",
@@ -322,13 +333,58 @@ async function updatePackageJson({ localDev, name, projectPath, task, versions,
322
333
  if (!packageJson.overrides) {
323
334
  packageJson.overrides = {};
324
335
  }
325
- // react-native-macos and react-native-windows may declare conflicting peer
326
- // dependency ranges to what the Expo template provides.
327
- if (packageJson.dependencies.react) {
328
- packageJson.overrides.react = packageJson.dependencies.react;
336
+ // The "react" version in expo-template-blank-typescript for SDK 54 is
337
+ // 19.1.0, but it needs to be 19.1.4 to satisfy react-native-macos and
338
+ // react-native-windows. We find that the React Native HelloWorld template
339
+ // is more reliable as a source of truth for the best "react" and
340
+ // "react-native" versions to satisfy them, but it's still a best of a bad
341
+ // job.
342
+ let facebookDeps;
343
+ try {
344
+ const packageJson = (await getReactNativePackageJson(versions.minor));
345
+ facebookDeps = {
346
+ react: packageJson.dependencies.react,
347
+ "react-native": packageJson.dependencies["react-native"],
348
+ };
349
+ }
350
+ catch (error) {
351
+ // It's totally possible to get rate-limited by the GitHub API, so we'll
352
+ // avoid failing the entire command just for a bad fetch here.
353
+ console.warn(`Unable to get the ideal dependency versions for "react" and "react-native" from the official React Native HelloWorld template, so will use the ones from the Expo template as-is.`, error);
354
+ }
355
+ if (facebookDeps) {
356
+ packageJson.dependencies.react = facebookDeps.react;
357
+ packageJson.overrides.react = facebookDeps.react;
358
+ packageJson.dependencies["react-native"] = facebookDeps["react-native"];
359
+ packageJson.overrides["react-native"] = facebookDeps["react-native"];
360
+ }
361
+ else {
362
+ // react-native-macos and react-native-windows may declare conflicting
363
+ // peer dependency ranges to what the Expo template provides.
364
+ if (packageJson.dependencies.react) {
365
+ packageJson.overrides.react = packageJson.dependencies.react;
366
+ }
367
+ if (packageJson.dependencies["react-native"]) {
368
+ packageJson.overrides["react-native"] = packageJson.dependencies["react-native"];
369
+ }
370
+ }
371
+ if (!packageJson.expo) {
372
+ packageJson.expo = {};
373
+ }
374
+ if (!packageJson.expo.install) {
375
+ packageJson.expo.install = {};
376
+ }
377
+ if (!packageJson.expo.install.exclude) {
378
+ packageJson.expo.install.exclude = [];
329
379
  }
330
- if (packageJson.dependencies["react-native"]) {
331
- packageJson.overrides["react-native"] = packageJson.dependencies["react-native"];
380
+ // Stop `expo start` erroneously warning about our overrides.
381
+ // - https://github.com/shirakaba/expo-desktop/issues/15
382
+ // - https://docs.expo.dev/versions/latest/config/package-json/#installexclude
383
+ if (!packageJson.expo.install.exclude.includes("react")) {
384
+ packageJson.expo.install.exclude.push("react");
385
+ }
386
+ if (!packageJson.expo.install.exclude.includes("react-native")) {
387
+ packageJson.expo.install.exclude.push("react-native");
332
388
  }
333
389
  }
334
390
  try {
@@ -340,6 +396,10 @@ async function updatePackageJson({ localDev, name, projectPath, task, versions,
340
396
  console.log(`${green("◆")} Altered package.json.\n`);
341
397
  return { name: nameBefore };
342
398
  }
399
+ async function getReactNativePackageJson(minor) {
400
+ const response = await fetch(`https://raw.githubusercontent.com/facebook/react-native/refs/heads/0.${minor}-stable/private/helloworld/package.json`);
401
+ return await response.json();
402
+ }
343
403
  async function npmInstall({ asNewWorkspace, cwd, packageManager, }) {
344
404
  const command = packageManager;
345
405
  const args = ["install"];
@@ -585,6 +645,93 @@ module.exports = function (api) {
585
645
  }
586
646
  console.log(`\n${green("◆")} Wrote babel.config.js.\n`);
587
647
  }
648
+ /**
649
+ * Improve the App.tsx so that it's not white-on-white on macOS in dark mode.
650
+ * Here, we simply ensure that it's black-on-white in both light and dark modes.
651
+ *
652
+ * @see https://github.com/shirakaba/expo-desktop/pull/14#issuecomment-4614018796
653
+ * @see https://github.com/expo/expo/blob/sdk-54/templates/expo-template-blank-typescript/App.tsx
654
+ */
655
+ async function improveAppTsx({ projectPath }) {
656
+ const metroConfigPath = path.resolve(projectPath, "App.tsx");
657
+ console.log(`${cyan("◆")} Overwriting App.tsx…\n`);
658
+ try {
659
+ await fs.writeFile(metroConfigPath, `
660
+ import { StatusBar } from 'expo-status-bar';
661
+ import { StyleSheet, Text, View } from 'react-native';
662
+
663
+ export default function App() {
664
+ return (
665
+ <View style={styles.container}>
666
+ <Text style={styles.text}>Open up App.tsx to start working on your app!</Text>
667
+ <StatusBar style="auto" />
668
+ </View>
669
+ );
670
+ }
671
+
672
+ const styles = StyleSheet.create({
673
+ container: {
674
+ flex: 1,
675
+ backgroundColor: '#fff',
676
+ alignItems: 'center',
677
+ justifyContent: 'center',
678
+ },
679
+ text: {
680
+ color: '#000',
681
+ },
682
+ });
683
+ `.trim() + "\n", "utf-8");
684
+ }
685
+ catch (error) {
686
+ log.error(`Error improving ${yellow("App.tsx")} file${error instanceof Error ? `: ${error.message}` : "."}`);
687
+ process.exit(1);
688
+ }
689
+ console.log(`\n${green("◆")} Overwrote App.tsx.\n`);
690
+ }
691
+ /**
692
+ * Stage and commit all changes.
693
+ *
694
+ * @see https://github.com/expo/expo/blob/main/packages/create-expo/src/utils/git.ts
695
+ */
696
+ async function commitChanges({ projectPath }) {
697
+ if (!(await hasProjectGitRepositoryAsync(projectPath))) {
698
+ console.warn(`\n${yellow("⚠")} Skipping git commit: create-expo-app did not initialize a Git repository in this project (this happens when creating inside an existing repo, e.g. in a monorepo).\n`);
699
+ return;
700
+ }
701
+ try {
702
+ await tasks([
703
+ promisifiedSpawnTask({
704
+ title: "git add",
705
+ command: "git",
706
+ args: ["add", "-A"],
707
+ options: { cwd: projectPath, stdio: "ignore" },
708
+ }),
709
+ ]);
710
+ }
711
+ catch (error) {
712
+ log.error(`Error running ${yellow("git add -A")}${error instanceof Error ? `: ${error.message}` : "."}`);
713
+ process.exit(1);
714
+ }
715
+ if (!(await hasGitStagedChangesAsync(projectPath))) {
716
+ console.log(`\n${green("◆")} Nothing to commit.\n`);
717
+ return;
718
+ }
719
+ try {
720
+ await tasks([
721
+ promisifiedSpawnTask({
722
+ title: "git commit",
723
+ command: "git",
724
+ args: ["commit", "-m", "Set up with expo-desktop"],
725
+ options: { cwd: projectPath, stdio: "ignore" },
726
+ }),
727
+ ]);
728
+ }
729
+ catch (error) {
730
+ log.error(`Error running ${yellow("git commit")}${error instanceof Error ? `: ${error.message}` : "."}`);
731
+ process.exit(1);
732
+ }
733
+ console.log(`\n${green("◆")} Committed changes.\n`);
734
+ }
588
735
  /**
589
736
  * For now, this is just a convenience script to allow me to run the config
590
737
  * plugins again after creating the app, to help with development. But it is
@@ -696,3 +843,36 @@ async function applyConfigPlugins(options) {
696
843
  await applyConfigPlugins(info);
697
844
  `.trim() + "\n", "utf-8");
698
845
  }
846
+ function logProjectReady({ cdPath, packageManager, }) {
847
+ const lines = [
848
+ "✅ Your project is ready!",
849
+ "",
850
+ "To run your project, first navigate to the directory and start up the packager:",
851
+ "",
852
+ `- cd ${cdPath}`,
853
+ `- ${formatRunCommand(packageManager, "start")}`,
854
+ "",
855
+ "… and then run the target platform of your choice:",
856
+ "",
857
+ `- ${formatRunCommand(packageManager, "android")}`,
858
+ `- ${formatRunCommand(packageManager, "ios")}`,
859
+ `- ${formatRunCommand(packageManager, "web")}`,
860
+ `- ${formatRunCommand(packageManager, "macos")}`,
861
+ `- ${formatRunCommand(packageManager, "windows")}`,
862
+ "",
863
+ ];
864
+ log.success(lines.join("\n"), { withGuide: false });
865
+ }
866
+ export function formatRunCommand(packageManager, cmd) {
867
+ switch (packageManager) {
868
+ case "pnpm":
869
+ return `pnpm run ${cmd}`;
870
+ case "yarn":
871
+ return `yarn ${cmd}`;
872
+ case "bun":
873
+ return `bun run ${cmd}`;
874
+ case "npm":
875
+ default:
876
+ return `npm run ${cmd}`;
877
+ }
878
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-desktop",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Best-effort desktop support for Expo",
5
5
  "keywords": [
6
6
  "android",
@@ -37,8 +37,8 @@
37
37
  "@clack/prompts": "^1.2.0",
38
38
  "arktype": "^2.2.0",
39
39
  "citty": "^0.2.2",
40
- "expo-desktop-config-plugins": "^1.1.30",
41
- "expo-desktop-prebuild-config": "^1.0.16",
40
+ "expo-desktop-config-plugins": "^1.1.32",
41
+ "expo-desktop-prebuild-config": "^1.0.18",
42
42
  "glob": "^10.5.0",
43
43
  "kleur": "^4.1.5",
44
44
  "mustache": "^4.2.0",