expo-desktop 0.1.18 → 0.1.20

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/build/cli.js CHANGED
@@ -23,6 +23,11 @@ const main = defineCommand({
23
23
  description: `The ${kleur.bold("display name")} for the app ${grey("(Examples: 'My App 123', '俺のアプリ')")}`,
24
24
  valueHint: "name",
25
25
  },
26
+ "local-dev": {
27
+ type: "boolean",
28
+ description: "An undocumented switch for use during development to skip the questionnaire.",
29
+ hidden: true,
30
+ },
26
31
  rdns: {
27
32
  type: "string",
28
33
  description: `The ${kleur.bold("reverse DNS")} for the app ${grey("(Example: 'com.example.my-app-123')")}`,
@@ -0,0 +1,13 @@
1
+ import fs from "node:fs/promises";
2
+ /**
3
+ * Read the state of a file on first call of the generator, then run the
4
+ * generator again to restore it back to that value that was read.
5
+ */
6
+ export async function* preserveFile({ filePath, enable, }) {
7
+ if (!enable) {
8
+ return;
9
+ }
10
+ const fileBefore = await fs.readFile(filePath, "utf-8");
11
+ yield;
12
+ await fs.writeFile(filePath, fileBefore, "utf-8");
13
+ }
@@ -3,20 +3,21 @@ import { default as kleur } from "kleur";
3
3
  import { green, grey } from "kleur/colors";
4
4
  import { platform } from "node:process";
5
5
  import { title } from "../common/clack.js";
6
- import { createExpoDesktopApp, localDev } from "./create-expo-desktop-app.js";
6
+ import { createExpoDesktopApp } from "./create-expo-desktop-app.js";
7
7
  import { previewFileTree } from "./preview-file-tree.js";
8
8
  import { promptForVersion } from "./prompt-for-version.js";
9
9
  export async function newExpoDesktopProject(args) {
10
- // A dev-time switch for skipping the questions
11
- const skip = localDev;
12
- if (skip) {
10
+ // A switch for skipping the questions
11
+ const localDev = args["local-dev"];
12
+ if (localDev) {
13
13
  await createExpoDesktopApp({
14
+ localDev,
14
15
  name: {
15
16
  displayName: "Your App Display Name",
16
17
  filesafeName: "YourApp456",
17
18
  rdns: "uk.co.birchlabs.your-app-456",
18
19
  },
19
- packageManager: "bun",
20
+ packageManager: "pnpm",
20
21
  templates: {
21
22
  template: args.template,
22
23
  "template-ios": args["template-ios"],
@@ -57,6 +58,7 @@ export async function newExpoDesktopProject(args) {
57
58
  process.exit(0);
58
59
  }
59
60
  await createExpoDesktopApp({
61
+ localDev,
60
62
  name,
61
63
  packageManager,
62
64
  versions,
@@ -11,18 +11,10 @@ import { makePrettySummary } from "../common/arktype.js";
11
11
  import { promisifiedSpawnTask, SPAWN_DEBUG_LOG_GLOB } from "../common/child-process.js";
12
12
  import { title } from "../common/clack.js";
13
13
  import { packageManagerExec } from "../common/npm.js";
14
+ import { preserveFile } from "../common/preserve-file.js";
14
15
  import { applySelectedTemplatesAsync } from "../common/template.js";
15
- /**
16
- * A crude switch to use to help with local development.
17
- *
18
- * - Skips the questionnaire at the start.
19
- * - Installs the local copy of expo-desktop-config-plugins rather than pinning
20
- * to a published release.
21
- * - Adds the apply-config-plugins.mjs script.
22
- */
23
- export const localDev = false;
24
- export async function createExpoDesktopApp({ name, packageManager, templates, versions, }) {
25
- const { projectPath } = await createExpoApp({ name, packageManager, versions });
16
+ export async function createExpoDesktopApp({ localDev, name, packageManager, templates, versions, }) {
17
+ const { projectPath } = await createExpoApp({ localDev, name, packageManager, versions });
26
18
  await appendRootGitignoreSpawnDebugLogs(projectPath);
27
19
  const templateSelection = {
28
20
  // https://github.com/expo/expo/blob/sdk-54/templates/expo-template-blank-typescript
@@ -54,13 +46,14 @@ export async function createExpoDesktopApp({ name, packageManager, templates, ve
54
46
  await updateAppJson({ name, projectPath });
55
47
  title("Altering package.json…", { spacing: 1 });
56
48
  const { name: packageJsonName } = await updatePackageJson({
49
+ localDev,
57
50
  name,
58
51
  projectPath,
59
52
  versions,
60
53
  task: { type: "create" },
61
54
  });
62
55
  title("Installing dependencies…", { spacing: 1 });
63
- await npmInstall({ cwd: projectPath, packageManager });
56
+ await npmInstall({ asNewWorkspace: true, cwd: projectPath, packageManager });
64
57
  await updatePackageJson({
65
58
  name,
66
59
  projectPath,
@@ -94,13 +87,37 @@ export async function createExpoDesktopApp({ name, packageManager, templates, ve
94
87
  title("Installing Cocoapods for the macOS app…", { spacing: 1 });
95
88
  await podInstall({ projectPath, type: "macos" });
96
89
  }
90
+ // react-native config seems to return `windows: null` on non-Windows
91
+ // platforms (while returning a populated object for Windows). If you force it
92
+ // to non-null for development on macOS, macOS runs the autolinking command
93
+ // successfully but writes out erroneous output, so
94
+ // there's no point.
95
+ if (platform === "win32") {
96
+ title("Autolinking the Windows app…", { spacing: 1 });
97
+ await autolinkWindows({ projectPath });
98
+ }
97
99
  title("Adding Expo support to the Metro config…", { spacing: 1 });
98
100
  await improveMetroConfig({ projectPath });
101
+ await addWindowsExpoPolyfill({ projectPath });
99
102
  title("Adding Expo support to the Babel config…", { spacing: 1 });
100
103
  await writeBabelConfig({ projectPath });
101
- // TODO: Set up Windows app.cpp entrypoint
102
104
  }
103
- async function createExpoApp({ name, packageManager, versions, }) {
105
+ async function createExpoApp({ localDev, name, packageManager, versions, }) {
106
+ // `create-expo-app` aggravatingly reconfigures your workspace to use
107
+ // `nodeLinker: hoisted`, which sucks when creating sample projects inside
108
+ // this monorepo during local dev (even with `--no-install`). So we fight
109
+ // back.
110
+ //
111
+ // For non-local dev, it sounds like we can use `nodeLinker: isolated` as of
112
+ // Expo SDK 54, so I'm tempted to enforce that in created templates, too. But
113
+ // one thing at a time.
114
+ // - https://docs.expo.dev/more/create-expo/#pnpm
115
+ // - https://github.com/expo/expo/blob/222b3b12610d69784bab6c5a188a46ea388f866a/packages/create-expo/src/resolvePackageManager.ts#L109
116
+ const gen = preserveFile({
117
+ enable: localDev,
118
+ filePath: localDev ? path.resolve(import.meta.dirname, "../../../../pnpm-workspace.yaml") : "",
119
+ });
120
+ await gen.next();
104
121
  // `npm create` drops flags meant for create-expo-app unless you add `--`; use
105
122
  // `npx --yes` instead to forward args correctly and skip prompts.
106
123
  const command = packageManager === "npm" ? "npx" : packageManager;
@@ -133,6 +150,9 @@ async function createExpoApp({ name, packageManager, versions, }) {
133
150
  log.error(`Error running ${yellow("create expo-app")}${error instanceof Error ? `: ${error.message}` : "."}`);
134
151
  process.exit(1);
135
152
  }
153
+ finally {
154
+ await gen.next();
155
+ }
136
156
  return { projectPath };
137
157
  }
138
158
  async function appendRootGitignoreSpawnDebugLogs(projectPath) {
@@ -209,7 +229,7 @@ async function updateAppJson({ name, projectPath, }) {
209
229
  }
210
230
  console.log(`${green("◆")} Altered app.json.\n`);
211
231
  }
212
- async function updatePackageJson({ name, projectPath, task, versions, }) {
232
+ async function updatePackageJson({ localDev, name, projectPath, task, versions, }) {
213
233
  const packageJsonPath = path.resolve(projectPath, "package.json");
214
234
  let packageJson;
215
235
  try {
@@ -238,19 +258,21 @@ async function updatePackageJson({ name, projectPath, task, versions, }) {
238
258
  }
239
259
  packageJson.scripts.macos = "rnc-cli run-macos";
240
260
  packageJson.scripts.windows = "rnc-cli run-windows";
261
+ packageJson.scripts["autolink-windows"] = "react-native autolink-windows";
241
262
  }
242
263
  else {
243
264
  if (!packageJson.dependencies) {
244
265
  packageJson.dependencies = {};
245
266
  }
246
- // expo-desktop-prebuild-config itself depends on
247
- // expo-desktop-config-plugins.
248
- if (localDev) {
249
- packageJson.dependencies["expo-desktop-prebuild-config"] =
250
- "file:../../expo-desktop-prebuild-config";
251
- }
252
- else {
253
- packageJson.dependencies["expo-desktop-prebuild-config"] = "^1.0.0";
267
+ const monorepoDeps = {
268
+ // expo-desktop-prebuild-config itself depends on
269
+ // expo-desktop-config-plugins.
270
+ ["expo-desktop-prebuild-config"]: "^1.0.0",
271
+ ["expo-desktop-modules-core"]: `^${versions.expoMajor}.0.0`,
272
+ ["expo-desktop-stubs"]: `^${versions.expoMajor}.0.0`,
273
+ };
274
+ for (const [key, value] of Object.entries(monorepoDeps)) {
275
+ packageJson.dependencies[key] = localDev ? `file:../../${key}` : value;
254
276
  }
255
277
  packageJson.dependencies["react-native-macos"] = versions.macos;
256
278
  packageJson.dependencies["react-native-windows"] = versions.windows;
@@ -281,10 +303,41 @@ async function updatePackageJson({ name, projectPath, task, versions, }) {
281
303
  console.log(`${green("◆")} Altered package.json.\n`);
282
304
  return { name: nameBefore };
283
305
  }
284
- async function npmInstall({ cwd, packageManager, }) {
306
+ async function npmInstall({ asNewWorkspace, cwd, packageManager, }) {
285
307
  const command = packageManager;
286
308
  const args = ["install"];
287
309
  console.log(`${cyan("◆")} Running: ${yellow(`${command} ${args.join(" ")}`)}\n`);
310
+ // Unlike npm and bun, pnpm climbs up to install dependencies in the closest
311
+ // ancestor directory if there is one. This is particularly inconvenient
312
+ // during local dev when we're creating samples inside the monorepo.
313
+ if (asNewWorkspace && packageManager === "pnpm") {
314
+ // (1) Ensure a file name pnpm-workspace.yaml exists.
315
+ //
316
+ // (2) Also ensure that it uses nodeLinker: hoisted, as otherwise
317
+ // `:path => "#{config[:reactNativePath]}-macos"` predicts that there will
318
+ // be a react-native-macos directory right beside the react-native
319
+ // directory by just optimistically appending "-macos" on the end, like so:
320
+ // "../node_modules/.pnpm/react-native@0.81.5_@babel+core@7.29.0_@react-native-community+cli@20.1.3_typescript@5._941d99d35895d1a3626e14fd9f3b3666/node_modules/react-native" + "-macos"
321
+ //
322
+ // As this is not true with pnpm's default `nodeLinker: isolated`, we
323
+ // need to stick to `nodeLinker: hoisted` until we can rewrite the
324
+ // Podfile script to resolve it properly.
325
+ //
326
+ // This is consistent with what the Expo team do for pnpm and yarn:
327
+ // - https://docs.expo.dev/more/create-expo/#pnpm
328
+ // - https://github.com/expo/expo/blob/222b3b12610d69784bab6c5a188a46ea388f866a/packages/create-expo/src/resolvePackageManager.ts#L109
329
+ try {
330
+ await fs.writeFile(path.resolve(cwd, "pnpm-workspace.yaml"), "nodeLinker: hoisted\n", {
331
+ flag: "wx",
332
+ encoding: "utf-8",
333
+ });
334
+ }
335
+ catch (error) {
336
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
337
+ throw error;
338
+ }
339
+ }
340
+ }
288
341
  try {
289
342
  await tasks([
290
343
  promisifiedSpawnTask({
@@ -386,6 +439,27 @@ async function podInstall({ projectPath, type }) {
386
439
  }
387
440
  console.log(`\n${green("◆")} Installed Cocoapods for the ${type === "ios" ? "iOS" : "macOS"} app.\n`);
388
441
  }
442
+ async function autolinkWindows({ projectPath }) {
443
+ const command = "node";
444
+ const args = ["--run", "autolink-windows"];
445
+ const printedCommand = `${command} ${args.join(" ")}`;
446
+ console.log(`${cyan("◆")} Running: ${yellow(printedCommand)}\n`);
447
+ try {
448
+ await tasks([
449
+ promisifiedSpawnTask({
450
+ title: "react-native autolink-windows",
451
+ command,
452
+ args,
453
+ options: { cwd: projectPath, stdio: "inherit" },
454
+ }),
455
+ ]);
456
+ }
457
+ catch (error) {
458
+ log.error(`Error running ${yellow(printedCommand)}${error instanceof Error ? `: ${error.message}` : "."}`);
459
+ process.exit(1);
460
+ }
461
+ console.log(`\n${green("◆")} Autolinked the Windows app.\n`);
462
+ }
389
463
  async function improveMetroConfig({ projectPath }) {
390
464
  const metroConfigPath = path.resolve(projectPath, "metro.config.js");
391
465
  console.log(`${cyan("◆")} Overwriting metro.config.js…\n`);
@@ -395,6 +469,17 @@ const { getDefaultConfig } = require("@expo/metro-config");
395
469
  const { makeMetroConfig } = require("@rnx-kit/metro-config");
396
470
 
397
471
  const config = makeMetroConfig(getDefaultConfig(__dirname));
472
+
473
+ const getPolyfills = config.serializer.getPolyfills;
474
+ const windowsExpoPolyfill = require.resolve("./expo-polyfill.windows.js");
475
+ config.serializer.getPolyfills = (platform) => {
476
+ const polyfills = getPolyfills(platform);
477
+ if (platform === "windows") {
478
+ polyfills.push(windowsExpoPolyfill);
479
+ }
480
+ return polyfills;
481
+ };
482
+
398
483
  module.exports = config;
399
484
  `.trim() + "\n", "utf-8");
400
485
  }
@@ -404,6 +489,69 @@ module.exports = config;
404
489
  }
405
490
  console.log(`\n${green("◆")} Overwrote metro.config.js.\n`);
406
491
  }
492
+ /**
493
+ */
494
+ async function addWindowsExpoPolyfill({ projectPath }) {
495
+ await fs.writeFile(path.resolve(projectPath, "expo-polyfill.windows.js"), `
496
+ try {
497
+ // Until we can configure TurboModules for eager initialisation (which is
498
+ // waiting on https://github.com/microsoft/react-native-windows/pull/16093), we
499
+ // need to trigger the lazy-init of our TurboModule by accessing it for the
500
+ // first time, which causes the TurboModuleManager to call its REACT_INIT
501
+ // method.
502
+ //
503
+ // We can't do any imports inside getPolyfills(), but can use the global proxy:
504
+ globalThis.nativeModuleProxy.ExpoMainRuntimeInstaller;
505
+
506
+ // TODO: Implement Expo's NativeModulesProxy and make all of this
507
+ // lazy-initialised (and actually populate \`exportedMethods\`, etc.).
508
+
509
+ // Below are temporary stubs to suppress these warnings:
510
+ // WARN The "EXNativeModulesProxy" native module is not exported through NativeModules; verify that expo-modules-core's native code is linked properly
511
+ // WARN No native ExponentConstants module found, are you sure the expo-constants's module is linked properly?
512
+ // WARN No native ExponentConstants module found, are you sure the expo-constants's module is linked properly?
513
+
514
+ // Funnily enough, although expo-modules-core prefers to access
515
+ // global.expo?.modules?.NativeModulesProxy over the deprecated
516
+ // NativeModules?.NativeUnimoduleProxy, we still need the latter to exist in
517
+ // order for it to check the former. And it's only
518
+ globalThis.nativeModuleProxy.NativeUnimoduleProxy;
519
+ const { ExpoAsset } = globalThis.nativeModuleProxy;
520
+
521
+ const ExponentConstants = {};
522
+
523
+ globalThis.expo.modules = {
524
+ ExpoAsset,
525
+ ExponentConstants,
526
+ // - JS: apps/demo/node_modules/expo-modules-core/src/NativeModulesProxy.native.ts
527
+ // - iOS:
528
+ // - NativeUnimoduleProxy: apps/demo/node_modules/expo-modules-core/ios/Legacy/NativeModulesProxy/NativeModulesProxyModule.swift
529
+ // - NativeModulesProxy: apps/demo/node_modules/expo-modules-core/ios/Legacy/NativeModulesProxy/EXNativeModulesProxy.mm
530
+ // - Android:
531
+ // - NativeUnimoduleProxy: apps/demo/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
532
+ // - NativeModulesProxy: apps/demo/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/defaultmodules/NativeModulesProxyModule.kt
533
+ NativeModulesProxy: {
534
+ exportedMethods: {
535
+ ExpoAsset: [],
536
+ ExponentConstants: [],
537
+ },
538
+ modulesConstants: {
539
+ ExpoAsset: [],
540
+ ExponentConstants: [],
541
+ },
542
+ },
543
+ };
544
+
545
+ // Now expo-modules-core can populate its fileprivate
546
+ // \`const NativeModulesProxy: Record<string, ProxyNativeModule = {}\` from our
547
+ // global.expo?.modules?.NativeModulesProxy. This avoids the warning about
548
+ // EXNativeModulesProxy being missing.
549
+ } catch (error) {
550
+ console.error("Polyfill failed", error);
551
+ throw error;
552
+ }
553
+ `.trim() + "\n", "utf-8");
554
+ }
407
555
  async function updatePodfile({ projectPath }) {
408
556
  const appJsonPath = path.resolve(projectPath, "macos/Podfile");
409
557
  let contents;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-desktop",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Best-effort desktop support for Expo",
5
5
  "keywords": [
6
6
  "android",
@@ -41,8 +41,8 @@
41
41
  "kleur": "^4.1.5",
42
42
  "mustache": "^4.2.0",
43
43
  "toml": "^4.1.1",
44
- "expo-desktop-config-plugins": "1.1.18",
45
- "expo-desktop-prebuild-config": "1.0.7"
44
+ "expo-desktop-config-plugins": "1.1.20",
45
+ "expo-desktop-prebuild-config": "1.0.9"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@expo/config": "^12.0.13",