expo-desktop 0.1.3 → 0.1.5
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/{src/add-app/command.ts → build/add-app/command.js} +2 -3
- package/build/cli.js +86 -0
- package/build/common/app-config.js +44 -0
- package/build/common/app-json.js +32 -0
- package/build/common/apply-config-plugins.js +48 -0
- package/build/common/arktype.js +12 -0
- package/build/common/child-process.js +141 -0
- package/build/common/clack.js +9 -0
- package/build/common/define-config.js +42 -0
- package/build/common/npm.js +143 -0
- package/build/common/with-internal.js +13 -0
- package/build/create-app/command.js +124 -0
- package/build/create-app/create-expo-desktop-app.js +578 -0
- package/{src/create-app/preview-file-tree.ts → build/create-app/preview-file-tree.js} +8 -10
- package/build/create-app/prompt-for-version.js +330 -0
- package/build/prebuild/command.js +69 -0
- package/build/prebuild/resolve-options.js +18 -0
- package/package.json +7 -9
- package/src/cli.ts +0 -92
- package/src/common/app-config.ts +0 -114
- package/src/common/app-json.ts +0 -35
- package/src/common/apply-config-plugins.ts +0 -76
- package/src/common/arktype.ts +0 -19
- package/src/common/child-process.ts +0 -55
- package/src/common/clack.ts +0 -11
- package/src/common/define-config.ts +0 -51
- package/src/common/npm.ts +0 -199
- package/src/common/with-internal.ts +0 -19
- package/src/create-app/command.ts +0 -159
- package/src/create-app/create-expo-desktop-app.ts +0 -754
- package/src/create-app/prompt-for-version.ts +0 -465
- package/src/prebuild/command.ts +0 -99
- package/src/prebuild/resolve-options.ts +0 -34
package/build/cli.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { default as kleur } from "kleur";
|
|
4
|
+
import { dim, grey } from "kleur/colors";
|
|
5
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
6
|
+
const main = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "expo-desktop",
|
|
9
|
+
version: packageJson.version,
|
|
10
|
+
description: "Best-effort desktop support for Expo",
|
|
11
|
+
},
|
|
12
|
+
subCommands: {
|
|
13
|
+
"create-app": defineCommand({
|
|
14
|
+
meta: { name: "create-app", description: "Create a new Expo Desktop project" },
|
|
15
|
+
args: {
|
|
16
|
+
"filesafe-name": {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: `The ${kleur.bold("filesafe name")} for the app in alphanumeric format ${grey("(Example: 'MyApp123')")}`,
|
|
19
|
+
valueHint: "name",
|
|
20
|
+
},
|
|
21
|
+
"display-name": {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: `The ${kleur.bold("display name")} for the app ${grey("(Examples: 'My App 123', '俺のアプリ')")}`,
|
|
24
|
+
valueHint: "name",
|
|
25
|
+
},
|
|
26
|
+
rdns: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: `The ${kleur.bold("reverse DNS")} for the app ${grey("(Example: 'com.example.my-app-123')")}`,
|
|
29
|
+
valueHint: "name",
|
|
30
|
+
},
|
|
31
|
+
version: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: `The ${kleur.bold("minor version")} of React Native to align on ${grey("(Examples: '0.80', 'latest')")}`,
|
|
34
|
+
valueHint: "version",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async run({ args }) {
|
|
38
|
+
(await import("./create-app/command.js")).newExpoDesktopProject(args);
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
prebuild: defineCommand({
|
|
42
|
+
meta: { name: "prebuild", description: "Prebuild an Expo Desktop project" },
|
|
43
|
+
args: {
|
|
44
|
+
clean: {
|
|
45
|
+
type: "boolean",
|
|
46
|
+
description: "Delete the native folders and regenerate them before applying changes",
|
|
47
|
+
},
|
|
48
|
+
"no-install": {
|
|
49
|
+
type: "boolean",
|
|
50
|
+
description: "Skip installing npm packages and CocoaPods",
|
|
51
|
+
},
|
|
52
|
+
npm: {
|
|
53
|
+
type: "boolean",
|
|
54
|
+
description: `Use npm to install dependencies. ${dim("(Default when package-lock.json exists)")}`,
|
|
55
|
+
},
|
|
56
|
+
yarn: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
description: `Use yarn to install dependencies. ${dim("(Default when yarn.lock exists)")}`,
|
|
59
|
+
},
|
|
60
|
+
bun: {
|
|
61
|
+
type: "boolean",
|
|
62
|
+
description: `Use bun to install dependencies. ${dim("(Default when bun.lock exists)")}`,
|
|
63
|
+
},
|
|
64
|
+
pnpm: {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
description: `Use pnpm to install dependencies. ${dim("(Default when pnpm-lock.yaml exists)")}`,
|
|
67
|
+
},
|
|
68
|
+
template: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Project template to clone from. File path pointing to a local tar file, npm package or a github repo",
|
|
71
|
+
valueHint: "template",
|
|
72
|
+
},
|
|
73
|
+
platform: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: `Platforms to sync: macos, windows, desktop ${dim("(Default: desktop)")}`,
|
|
76
|
+
valueHint: "desktop|macos|windows",
|
|
77
|
+
alias: "p",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
async run({ args }) {
|
|
81
|
+
(await import("./prebuild/command.js")).prebuild(args);
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
await runMain(main);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
export const SemverMajorMinorOnly = type("/^(\\d+)\\.(\\d+)$/");
|
|
3
|
+
export const Dns = type("/^[a-zA-Z][a-zA-Z0-9]*(?:\\.[a-zA-Z][a-zA-Z0-9]*)*$/");
|
|
4
|
+
export const UnderscoredDns = type("/^[a-zA-Z]\\w*(?:\\.[a-zA-Z]\\w*)*$/");
|
|
5
|
+
export const HyphenatedDns = type("/^[a-zA-Z][a-zA-Z0-9\\-]*(?:\\.[a-zA-Z][a-zA-Z0-9\\-]*)*$/");
|
|
6
|
+
export const UnderscoredOrHyphenatedDns = type("/^[a-zA-Z][\\w\\-]*(?:\\.[a-zA-Z][\\w\\-]*)*$/");
|
|
7
|
+
function defineAppConfig({ includeDescriptions }) {
|
|
8
|
+
const description = (text) => (includeDescriptions ? { description: text } : {});
|
|
9
|
+
return type({
|
|
10
|
+
["react_native_version?"]: SemverMajorMinorOnly.configure(description("The `{major}.{minor}` version of `react-native` to align to, e.g. '0.82'. Defaults to the highest mutually supported across both `react-native-macos` and `react-native-windows`.")),
|
|
11
|
+
name: type({
|
|
12
|
+
alphanumeric: type("string.alphanumeric").configure(description("A **filesafe name** for the app consisting only of alphanumeric characters (A-z, and 0-9), e.g. 'MyApp123'. Will be used mainly for file names, e.g. 'MyApp123.xcodeproj'.")),
|
|
13
|
+
display_name: type("string").configure(description("The **display name** of your app, e.g. 'My App 123' or '俺のアプリ'. Accepts any string. Will be used as the app name on the iOS/Android home screen, in macOS Finder, and in Windows Explorer.")),
|
|
14
|
+
reverse_dns: UnderscoredOrHyphenatedDns.configure(description("The **reverse-Domain Name Specifier** for your app, e.g. 'com.example.my-app-123'. Accepts alphanumeric characters (A-z and 0-9), hyphens, and underscores. Will be used to fill in `android.package_namespace`, `android.application_id`, `windows.namespace`, `ios.bundle_identifier`, and `macos.bundle_identifier`. Hyphens and underscores will be sanitised as appropriate for these destinations, so don't worry which you use here.")),
|
|
15
|
+
}).configure(description("Different formats of the app name for use in different contexts.")),
|
|
16
|
+
["android?"]: type({
|
|
17
|
+
["application_id?"]: UnderscoredDns.configure(description("The **application ID** used on the Play Store, e.g. 'com.example.my_app_123'. Accepts only alphanumeric characters (A-z and 0-9), and underscores. Corresponds to `android.defaultConfig.applicationId` in `app/build.gradle`. Recommended to match `android.package_namespace`.")),
|
|
18
|
+
["package_namespace?"]: UnderscoredDns.configure(description("The **package namespace**, e.g. 'com.example.my_app_123'. Accepts only alphanumeric characters (A-z and 0-9), and underscores. Corresponds to `android.namespace` in `app/build.gradle`. Recommended to match `android.application_id`.")),
|
|
19
|
+
["root_project_name?"]: type("string").configure(description("The **display name** of your app, e.g. 'My App 123' or '俺のアプリ'. Accepts any string. Corresponds to `rootProject.name` in `settings.gradle`. Will be used as the app name on the Android home screen.")),
|
|
20
|
+
}).configure(description("Android-specific overrides for the default values (which are based on `name`).")),
|
|
21
|
+
["ios?"]: type({
|
|
22
|
+
["bundle_display_name?"]: type("string").configure(description("The **display name** of your app, e.g. 'My App 123' or '俺のアプリ'. Accepts any string. Corresponds to the `CFBundleDisplayName` key in the `Info.plist` file. Will be used as the app name on the iOS home screen.")),
|
|
23
|
+
["bundle_identifier?"]: HyphenatedDns.configure(description("The **bundle identifier** of your app, e.g. 'com.example.my-app-123'. Accepts only alphanumeric characters (A-z and 0-9), and hyphens. Corresponds to the `PRODUCT_BUNDLE_IDENTIFIER` Xcode build variable, used to fill in the `CFBundleIdentifier` key in the `Info.plist` file.")),
|
|
24
|
+
}).configure(description("iOS-specific overrides for the default values (which are based on `name`).")),
|
|
25
|
+
["macos?"]: type({
|
|
26
|
+
["bundle_display_name?"]: type("string").configure(description("The **display name** of your app, e.g. 'My App 123' or '俺のアプリ'. Accepts any string. Corresponds to the `PRODUCT_BUNDLE_IDENTIFIER` Xcode build variable, used to fill in the `CFBundleDisplayName` key in the `Info.plist` file. Will be used as the app name in macOS Finder.")),
|
|
27
|
+
["bundle_identifier?"]: HyphenatedDns.configure(description("The **bundle identifier** of your app, e.g. 'com.example.my-app-123'. Accepts only alphanumeric characters (A-z and 0-9), and hyphens. Corresponds to the `PRODUCT_BUNDLE_IDENTIFIER` Xcode build variable, used to fill in the `CFBundleIdentifier` key in the `Info.plist` file.")),
|
|
28
|
+
}).configure(description("macOS-specific overrides for the default values (which are based on `name`).")),
|
|
29
|
+
["windows?"]: type({
|
|
30
|
+
["display_name?"]: type("string").configure(description("The **display name** of your app, e.g. 'My App 123' or '俺のアプリ'. Accepts any string. Corresponds to the `ProjectName` value in the `.vcxproj` file. Will be used as the app name in Windows Explorer.")),
|
|
31
|
+
["namespace?"]: Dns.configure(description("The WinRT and C++ **namespace**, e.g. 'com.example.myapp123'. Accepts only alphanumeric characters (A-z and 0-9). When used in C++, `.` characters are converted to `::`, e.g. 'com::example::myapp123'.")),
|
|
32
|
+
["project_name?"]: type("string.alphanumeric").configure(description("A **filesafe name** for the app consisting only of alphanumeric characters (A-z, and 0-9), e.g. 'MyApp123'. Will be used mainly for file names, e.g. 'MyApp123.vcxproj'.")),
|
|
33
|
+
}).configure(description("Windows-specific overrides for the default values (which are based on `name`).")),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export const PartialAppConfig = defineAppConfig({ includeDescriptions: false });
|
|
37
|
+
export const PartialAppConfigForJsonSchema = defineAppConfig({ includeDescriptions: true });
|
|
38
|
+
const RequiredTopLevelAppConfig = type.keywords.Required(PartialAppConfig);
|
|
39
|
+
export const AppConfig = type.merge(RequiredTopLevelAppConfig, {
|
|
40
|
+
android: RequiredTopLevelAppConfig.get("android").required(),
|
|
41
|
+
ios: RequiredTopLevelAppConfig.get("ios").required(),
|
|
42
|
+
macos: RequiredTopLevelAppConfig.get("macos").required(),
|
|
43
|
+
windows: RequiredTopLevelAppConfig.get("windows").required(),
|
|
44
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
export const ExpoConfig = type({
|
|
3
|
+
"name?": "string",
|
|
4
|
+
"slug?": "string",
|
|
5
|
+
"platforms?": type('"android" | "ios" | "web" | "macos" | "windows"').array(),
|
|
6
|
+
"version?": "string",
|
|
7
|
+
"ios?": type({
|
|
8
|
+
"bundleIdentifier?": "string",
|
|
9
|
+
}),
|
|
10
|
+
"macos?": type({
|
|
11
|
+
"bundleIdentifier?": "string",
|
|
12
|
+
}),
|
|
13
|
+
// "windows?": type({
|
|
14
|
+
// "projectName?": "string",
|
|
15
|
+
// "displayName?": "string",
|
|
16
|
+
// "namespace?": "string",
|
|
17
|
+
// }),
|
|
18
|
+
"android?": type({
|
|
19
|
+
"package?": "string",
|
|
20
|
+
}),
|
|
21
|
+
"plugins?": type(type(["string", "...", "unknown[]"]).or("string")).array(),
|
|
22
|
+
});
|
|
23
|
+
export const AppJson = type({
|
|
24
|
+
"expo?": ExpoConfig,
|
|
25
|
+
});
|
|
26
|
+
export const PackageJson = type({
|
|
27
|
+
"name?": "string",
|
|
28
|
+
"scripts?": "Record<string, string>",
|
|
29
|
+
"dependencies?": "Record<string, string>",
|
|
30
|
+
"devDependencies?": "Record<string, string>",
|
|
31
|
+
"peerDependencies?": "Record<string, string>",
|
|
32
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { withInternal } from "./with-internal.js";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { getPrebuildConfigAsync } = require("expo-desktop-prebuild-config");
|
|
6
|
+
const { compileModsAsync } = require("expo-desktop-config-plugins");
|
|
7
|
+
/**
|
|
8
|
+
* Applies config plugins.
|
|
9
|
+
* @see https://github.com/microsoft/react-native-test-app/blob/trunk/packages/app/scripts/config-plugins/apply.mjs
|
|
10
|
+
*/
|
|
11
|
+
export async function applyConfigPlugins(options) {
|
|
12
|
+
const { projectRoot } = options;
|
|
13
|
+
// To avoid making expo-desktop depend on Expo SDK 54 when we might be running
|
|
14
|
+
// on an Expo 55 project, we import Expo deps from the project itself.
|
|
15
|
+
let expoConfigModule;
|
|
16
|
+
try {
|
|
17
|
+
expoConfigModule = require(path.dirname(require.resolve("@expo/config/package.json", { paths: [projectRoot] })));
|
|
18
|
+
}
|
|
19
|
+
catch (cause) {
|
|
20
|
+
throw new Error(`Error importing "@expo/config" relative to projectRoot "${projectRoot}". Make sure to install node modules before running any prebuilds, and make sure that the project depends on the package named "expo".`, { cause });
|
|
21
|
+
}
|
|
22
|
+
const { getConfig } = expoConfigModule;
|
|
23
|
+
let expoConfigPluginsModule;
|
|
24
|
+
try {
|
|
25
|
+
expoConfigPluginsModule = require(path.dirname(require.resolve("@expo/config-plugins/package.json", { paths: [projectRoot] })));
|
|
26
|
+
}
|
|
27
|
+
catch (cause) {
|
|
28
|
+
throw new Error(`Error importing "@expo/config-plugins" relative to projectRoot "${projectRoot}". Make sure to install node modules before running any prebuilds, and make sure that the project depends on the package named "expo".`, { cause });
|
|
29
|
+
}
|
|
30
|
+
const { withPlugins } = expoConfigPluginsModule;
|
|
31
|
+
// (1) Filter out platforms that aren't in the app.json.
|
|
32
|
+
// https://github.com/expo/expo/blob/8dd645080f52927e2a8bf406167da7241a1d46d8/packages/%40expo/cli/src/prebuild/prebuildAsync.ts#L74
|
|
33
|
+
let { exp: expoConfig } = getConfig(projectRoot);
|
|
34
|
+
const { platforms, plugins } = expoConfig;
|
|
35
|
+
if (platforms?.length) {
|
|
36
|
+
const finalPlatforms = options.platforms.filter((platform) => platforms.includes(platform));
|
|
37
|
+
if (finalPlatforms.length > 0) {
|
|
38
|
+
options.platforms = finalPlatforms;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const requestedPlatforms = options.platforms.join(", ");
|
|
42
|
+
console.warn(`⚠️ Requested prebuild for "${requestedPlatforms}", but only "${platforms.join(", ")}" is present in app config ("expo.platforms" entry). Continuing with "${requestedPlatforms}".`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const prebuildConfig = await getPrebuildConfigAsync(projectRoot, options);
|
|
46
|
+
expoConfig = prebuildConfig.exp;
|
|
47
|
+
return compileModsAsync(withPlugins(withInternal(expoConfig, options), plugins), options);
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { green, grey, yellow } from "kleur/colors";
|
|
2
|
+
export function makePrettySummary({ byPath }) {
|
|
3
|
+
const summary = new Array();
|
|
4
|
+
for (const [path, error] of Object.entries(byPath)) {
|
|
5
|
+
const actualIndex = error.problem.lastIndexOf(" (was ");
|
|
6
|
+
const problem = actualIndex === -1
|
|
7
|
+
? error.problem
|
|
8
|
+
: `${error.problem.startsWith("must be matched by ") ? `must be matched by ${green(error.problem.slice("must be matched by ".length, actualIndex))}` : error.problem.slice(0, actualIndex)}${grey(error.problem.slice(actualIndex))}`;
|
|
9
|
+
summary.push(`${yellow(path)} ${problem}`);
|
|
10
|
+
}
|
|
11
|
+
return summary;
|
|
12
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { env } from "node:process";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
/**
|
|
7
|
+
* Clack {@link Task} that runs a subprocess; piped stdout/stderr lines are sent
|
|
8
|
+
* through the task `message` callback (not `log.message`). Lines are also kept in
|
|
9
|
+
* an interleaved buffer; on failure they are written under {@link debugLogDir} or
|
|
10
|
+
* {@link SpawnOptions.cwd} or the current working directory.
|
|
11
|
+
*/
|
|
12
|
+
export function promisifiedSpawnTask({ title, command, args, options = {}, debugLogDir, }) {
|
|
13
|
+
return {
|
|
14
|
+
title,
|
|
15
|
+
task: (message) => runPromisifiedSpawn({
|
|
16
|
+
command,
|
|
17
|
+
args,
|
|
18
|
+
options,
|
|
19
|
+
logLine: message,
|
|
20
|
+
...(debugLogDir !== undefined ? { debugLogDir } : {}),
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function runPromisifiedSpawn({ command, args, options, logLine, debugLogDir, }) {
|
|
25
|
+
const stdioEffective = effectiveStdioForSpawnCapture(options.stdio) ?? options.stdio;
|
|
26
|
+
const spawnOptions = {
|
|
27
|
+
...options,
|
|
28
|
+
stdio: stdioEffective,
|
|
29
|
+
env: envWithForcedColorIfPiped({ ...options, stdio: stdioEffective }),
|
|
30
|
+
};
|
|
31
|
+
const cp = spawn(command, args, spawnOptions);
|
|
32
|
+
/** Interleaved stdout/stderr lines in arrival order (tagged for readability). */
|
|
33
|
+
const lineBuffer = [];
|
|
34
|
+
const pushLine = (stream, line) => {
|
|
35
|
+
lineBuffer.push(`${stream}\t${line}`);
|
|
36
|
+
logLine(line);
|
|
37
|
+
};
|
|
38
|
+
const { stdout, stderr } = cp;
|
|
39
|
+
const outMode = Array.isArray(spawnOptions.stdio) ? spawnOptions.stdio.at(1) : spawnOptions.stdio;
|
|
40
|
+
const errMode = Array.isArray(spawnOptions.stdio) ? spawnOptions.stdio.at(2) : spawnOptions.stdio;
|
|
41
|
+
if (stdout && outMode !== "inherit" && outMode !== "ignore") {
|
|
42
|
+
readline.createInterface({ input: stdout }).on("line", (line) => pushLine("stdout", line));
|
|
43
|
+
}
|
|
44
|
+
if (stderr && errMode !== "inherit" && errMode !== "ignore") {
|
|
45
|
+
readline.createInterface({ input: stderr }).on("line", (line) => pushLine("stderr", line));
|
|
46
|
+
}
|
|
47
|
+
let cpError = null;
|
|
48
|
+
cp.on("error", (error) => {
|
|
49
|
+
if (!cpError) {
|
|
50
|
+
cpError = error;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
54
|
+
cp.on("close", (code, signal) => {
|
|
55
|
+
void (async () => {
|
|
56
|
+
if (!cpError && code === 0) {
|
|
57
|
+
resolve();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const logDir = resolveDebugLogDir(spawnOptions, debugLogDir);
|
|
61
|
+
const fileName = `expo-desktop-spawn-debug-${Date.now()}.log`;
|
|
62
|
+
const absPath = path.join(logDir, fileName);
|
|
63
|
+
const header = [
|
|
64
|
+
"# Captured by expo-desktop when a subprocess failed",
|
|
65
|
+
`command: ${shellQuote(command)} ${args.map(shellQuote).join(" ")}`,
|
|
66
|
+
`cwd (spawn): ${spawnOptions.cwd !== undefined ? String(spawnOptions.cwd) : "(default)"}`,
|
|
67
|
+
`debug log directory: ${logDir}`,
|
|
68
|
+
`exit code: ${code === null ? "null" : code}`,
|
|
69
|
+
`signal: ${signal === null ? "null" : signal}`,
|
|
70
|
+
"---",
|
|
71
|
+
"",
|
|
72
|
+
].join("\n");
|
|
73
|
+
let wrotePath;
|
|
74
|
+
try {
|
|
75
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
76
|
+
const body = lineBuffer.length > 0
|
|
77
|
+
? lineBuffer.join("\n")
|
|
78
|
+
: "(no stdout/stderr lines were captured; streams may have been inherited or ignored.)";
|
|
79
|
+
await fs.writeFile(absPath, `${header}${body}\n`, "utf-8");
|
|
80
|
+
wrotePath = absPath;
|
|
81
|
+
}
|
|
82
|
+
catch (writeErr) {
|
|
83
|
+
reject(new Error(`Exited with code ${code} (signal: ${signal}). Failed to write debug log to ${absPath}: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}${cpError ? ` (${cpError.message})` : ""}`, cpError ? { cause: cpError } : writeErr instanceof Error ? { cause: writeErr } : {}));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
reject(new Error(`Exited with code ${code} (signal: ${signal}). Captured subprocess output was saved for debugging:\n ${wrotePath}` +
|
|
87
|
+
(cpError ? `\nUnderlying error: ${cpError.message}` : ""), cpError ? { cause: cpError } : {}));
|
|
88
|
+
})();
|
|
89
|
+
});
|
|
90
|
+
return promise;
|
|
91
|
+
}
|
|
92
|
+
function shellQuote(arg) {
|
|
93
|
+
if (/^[\w@%+=:,./-]+$/i.test(arg)) {
|
|
94
|
+
return arg;
|
|
95
|
+
}
|
|
96
|
+
return `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
97
|
+
}
|
|
98
|
+
function resolveDebugLogDir(options, debugLogDir) {
|
|
99
|
+
if (debugLogDir !== undefined) {
|
|
100
|
+
return path.resolve(process.cwd(), debugLogDir);
|
|
101
|
+
}
|
|
102
|
+
if (options.cwd !== undefined) {
|
|
103
|
+
return path.resolve(process.cwd(), String(options.cwd));
|
|
104
|
+
}
|
|
105
|
+
return process.cwd();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* When stdout and stderr are both inherited from the parent, switch them to pipes
|
|
109
|
+
* so we can buffer lines while still forwarding each line through {@link logLine}.
|
|
110
|
+
*/
|
|
111
|
+
function effectiveStdioForSpawnCapture(stdio) {
|
|
112
|
+
if (stdio === "inherit") {
|
|
113
|
+
return ["inherit", "pipe", "pipe"];
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(stdio) && stdio[1] === "inherit" && stdio[2] === "inherit") {
|
|
116
|
+
const head = stdio.slice(0, 1);
|
|
117
|
+
const tail = stdio.slice(3);
|
|
118
|
+
return [...head, "pipe", "pipe", ...tail];
|
|
119
|
+
}
|
|
120
|
+
return stdio;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* When stdio is piped, the child sees non-TTY streams and most color libraries
|
|
124
|
+
* disable ANSI.
|
|
125
|
+
*/
|
|
126
|
+
function envWithForcedColorIfPiped(options) {
|
|
127
|
+
const stdio = options?.stdio;
|
|
128
|
+
const stdoutMode = Array.isArray(stdio) ? stdio.at(1) : stdio;
|
|
129
|
+
const stderrMode = Array.isArray(stdio) ? stdio.at(2) : stdio;
|
|
130
|
+
const capturesOutput = stdoutMode !== "inherit" || stderrMode !== "inherit";
|
|
131
|
+
const base = { ...env, ...options?.env };
|
|
132
|
+
if (!capturesOutput || base.NO_COLOR !== undefined) {
|
|
133
|
+
return base;
|
|
134
|
+
}
|
|
135
|
+
if (base.FORCE_COLOR !== undefined && base.FORCE_COLOR !== "") {
|
|
136
|
+
return base;
|
|
137
|
+
}
|
|
138
|
+
return { ...base, FORCE_COLOR: "1" };
|
|
139
|
+
}
|
|
140
|
+
/** Gitignore glob for spawn debug logs. */
|
|
141
|
+
export const SPAWN_DEBUG_LOG_GLOB = "expo-desktop-spawn-debug*.log";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import kleur from "kleur";
|
|
3
|
+
export function title(text, opts) {
|
|
4
|
+
const { spacing, ...rest } = opts ?? {};
|
|
5
|
+
log.info(kleur.bold(kleur.inverse(` ${text} `)), { withGuide: false, ...rest });
|
|
6
|
+
if (opts?.spacing) {
|
|
7
|
+
log.message("", { withGuide: false, spacing: Math.max(0, opts.spacing - 1) });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { AppConfig, PartialAppConfig } from "./app-config.js";
|
|
3
|
+
import { makePrettySummary } from "./arktype.js";
|
|
4
|
+
export function defineAppConfig(config) {
|
|
5
|
+
const partial = PartialAppConfig(config);
|
|
6
|
+
if (partial instanceof type.errors) {
|
|
7
|
+
// console.log(`Invalid config:\n${makePrettySummary(partial).join("\n")}`);
|
|
8
|
+
throw new Error(`Invalid config:\n${makePrettySummary(partial).join("\n")}`);
|
|
9
|
+
}
|
|
10
|
+
const { name: { alphanumeric, display_name, reverse_dns }, } = partial;
|
|
11
|
+
const full = AppConfig({
|
|
12
|
+
...partial,
|
|
13
|
+
// TODO: Dynamically grab latest minor version
|
|
14
|
+
react_native_version: "0.82",
|
|
15
|
+
android: {
|
|
16
|
+
application_id: reverse_dns.replaceAll("-", "_"),
|
|
17
|
+
package_namespace: reverse_dns.replaceAll("-", "_"),
|
|
18
|
+
root_project_name: display_name,
|
|
19
|
+
...partial.android,
|
|
20
|
+
},
|
|
21
|
+
ios: {
|
|
22
|
+
bundle_display_name: display_name,
|
|
23
|
+
bundle_identifier: reverse_dns.replaceAll("_", "-"),
|
|
24
|
+
...partial.ios,
|
|
25
|
+
},
|
|
26
|
+
macos: {
|
|
27
|
+
bundle_display_name: display_name,
|
|
28
|
+
bundle_identifier: reverse_dns.replaceAll("_", "-"),
|
|
29
|
+
...partial.macos,
|
|
30
|
+
},
|
|
31
|
+
windows: {
|
|
32
|
+
display_name: display_name,
|
|
33
|
+
namespace: reverse_dns.replaceAll(/[\-_]/g, ""),
|
|
34
|
+
project_name: alphanumeric,
|
|
35
|
+
...partial.windows,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (full instanceof type.errors) {
|
|
39
|
+
throw new Error(`Invalid config:\n${makePrettySummary(full).join("\n")}`);
|
|
40
|
+
}
|
|
41
|
+
return full;
|
|
42
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { makePrettySummary } from "./arktype.js";
|
|
3
|
+
export async function getPackageInfo(packageName) {
|
|
4
|
+
const res = await fetch(`https://registry.npmjs.org/${packageName}`);
|
|
5
|
+
const data = await res.json();
|
|
6
|
+
const response = NpmResponse(data);
|
|
7
|
+
if (response instanceof type.errors) {
|
|
8
|
+
// console.log(`Invalid config:\n${makePrettySummary(partial).join("\n")}`);
|
|
9
|
+
throw new Error(`Invalid config:\n${makePrettySummary(response).join("\n")}`);
|
|
10
|
+
}
|
|
11
|
+
return response;
|
|
12
|
+
}
|
|
13
|
+
export function filterVersions({ npmInfo, distTag, fromMajor, fromMinor, fromPatch, includePrereleases, }) {
|
|
14
|
+
const map = {};
|
|
15
|
+
const { "dist-tags": distTags, versions } = npmInfo;
|
|
16
|
+
if (distTag) {
|
|
17
|
+
const version = distTags[distTag];
|
|
18
|
+
const match = semverMatcher.exec(version);
|
|
19
|
+
if (match) {
|
|
20
|
+
const [fullMatch, major, minor, patch, prerelease, _buildmetadata] = match;
|
|
21
|
+
const majorInt = parseInt(major);
|
|
22
|
+
const minorInt = parseInt(minor);
|
|
23
|
+
const patchInt = parseInt(patch);
|
|
24
|
+
if (!map[majorInt][minorInt][patchInt]) {
|
|
25
|
+
map[majorInt][minorInt][patchInt] = {
|
|
26
|
+
prereleases: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!!prerelease) {
|
|
30
|
+
map[majorInt][minorInt][patchInt].prereleases.push(fullMatch);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
map[majorInt][minorInt][patchInt].release = fullMatch;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { filtered: [version], map };
|
|
37
|
+
}
|
|
38
|
+
const filtered = new Array();
|
|
39
|
+
for (const version of versions) {
|
|
40
|
+
const match = semverMatcher.exec(version);
|
|
41
|
+
if (!match) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const [fullMatch, major, minor, patch, prerelease, _buildmetadata] = match;
|
|
45
|
+
const majorInt = parseInt(major);
|
|
46
|
+
const minorInt = parseInt(minor);
|
|
47
|
+
const patchInt = parseInt(patch);
|
|
48
|
+
if (typeof fromMajor === "number" && majorInt < fromMajor) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (typeof fromMinor === "number" && minorInt < fromMinor) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (typeof fromPatch === "number" && patchInt < fromPatch) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!includePrereleases && !!prerelease) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
filtered.push(fullMatch);
|
|
61
|
+
if (!map[majorInt]) {
|
|
62
|
+
map[majorInt] = {};
|
|
63
|
+
}
|
|
64
|
+
if (!map[majorInt][minorInt]) {
|
|
65
|
+
map[majorInt][minorInt] = {};
|
|
66
|
+
}
|
|
67
|
+
if (!map[majorInt][minorInt][patchInt]) {
|
|
68
|
+
map[majorInt][minorInt][patchInt] = {
|
|
69
|
+
prereleases: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!!prerelease) {
|
|
73
|
+
map[majorInt][minorInt][patchInt].prereleases.push(fullMatch);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
map[majorInt][minorInt][patchInt].release = fullMatch;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { filtered, map };
|
|
80
|
+
}
|
|
81
|
+
export function getHighestStableMinors(map) {
|
|
82
|
+
const minorMap = {};
|
|
83
|
+
for (const major in map) {
|
|
84
|
+
const majorInt = parseInt(major);
|
|
85
|
+
if (!minorMap[majorInt]) {
|
|
86
|
+
minorMap[majorInt] = {};
|
|
87
|
+
}
|
|
88
|
+
for (const minor in map[major]) {
|
|
89
|
+
const minorInt = parseInt(minor);
|
|
90
|
+
for (const patch in map[major][minor]) {
|
|
91
|
+
const patchInt = parseInt(patch);
|
|
92
|
+
if (!map[major][minor][patch].release) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const current = minorMap[majorInt][minorInt];
|
|
96
|
+
if (!current) {
|
|
97
|
+
minorMap[majorInt][minorInt] = map[major][minor][patch].release;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const currentMatch = semverMatcher.exec(current);
|
|
101
|
+
if (!currentMatch) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const [, , , currentPatch] = currentMatch;
|
|
105
|
+
if (patchInt <= parseInt(currentPatch)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
minorMap[majorInt][minorInt] = `${major}.${minor}.${patch}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return minorMap;
|
|
113
|
+
}
|
|
114
|
+
const NpmResponse = type({
|
|
115
|
+
name: "string",
|
|
116
|
+
"dist-tags": "Record<string, string.semver>",
|
|
117
|
+
versions: type("Record<string, Record<string, unknown>>").pipe((record) => Object.keys(record)),
|
|
118
|
+
// Other potentially useful fields that we'll avoid needlessly validating for
|
|
119
|
+
// now:
|
|
120
|
+
// time: "Record<string, string.date.iso>",
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* Exposing the underlying pattern used by string.semver.
|
|
124
|
+
* @see https://semver.org/
|
|
125
|
+
*/
|
|
126
|
+
export const semverMatcher = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][\dA-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][\dA-Za-z-]*))*))?(?:\+([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?$/;
|
|
127
|
+
export function packageManagerExec(packageManager) {
|
|
128
|
+
const args = new Array();
|
|
129
|
+
let command;
|
|
130
|
+
switch (packageManager) {
|
|
131
|
+
case "bun":
|
|
132
|
+
command = "bunx";
|
|
133
|
+
break;
|
|
134
|
+
case "npm":
|
|
135
|
+
command = "npm";
|
|
136
|
+
args.push("dlx");
|
|
137
|
+
break;
|
|
138
|
+
case "pnpm":
|
|
139
|
+
command = "pnpm";
|
|
140
|
+
args.push("dlx");
|
|
141
|
+
}
|
|
142
|
+
return { args, command };
|
|
143
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @see https://github.com/microsoft/react-native-test-app/blob/0951cf5a3727c01d2ef25540eb796eb56b14ae04/packages/app/scripts/config-plugins/plugins/withInternal.mjs#L9
|
|
3
|
+
*/
|
|
4
|
+
export const withInternal = (config, internals) => {
|
|
5
|
+
// @ts-ignore TODO
|
|
6
|
+
config._internal = {
|
|
7
|
+
isDebug: false,
|
|
8
|
+
// @ts-ignore TODO
|
|
9
|
+
...config._internal,
|
|
10
|
+
...internals,
|
|
11
|
+
};
|
|
12
|
+
return config;
|
|
13
|
+
};
|