expo-desktop 0.1.13 → 0.1.14
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
|
@@ -33,6 +33,31 @@ const main = defineCommand({
|
|
|
33
33
|
description: `The ${kleur.bold("minor version")} of React Native to align on ${grey("(Examples: '0.80', 'latest')")}`,
|
|
34
34
|
valueHint: "version",
|
|
35
35
|
},
|
|
36
|
+
template: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Base template source (tarball, npm spec, or GitHub owner/repo#ref:subpath)",
|
|
39
|
+
valueHint: "template",
|
|
40
|
+
},
|
|
41
|
+
"template-ios": {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "iOS-specific template source",
|
|
44
|
+
valueHint: "template",
|
|
45
|
+
},
|
|
46
|
+
"template-android": {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Android-specific template source",
|
|
49
|
+
valueHint: "template",
|
|
50
|
+
},
|
|
51
|
+
"template-macos": {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "macOS-specific template source",
|
|
54
|
+
valueHint: "template",
|
|
55
|
+
},
|
|
56
|
+
"template-windows": {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Windows-specific template source",
|
|
59
|
+
valueHint: "template",
|
|
60
|
+
},
|
|
36
61
|
},
|
|
37
62
|
async run({ args }) {
|
|
38
63
|
(await import("./create-app/command.js")).newExpoDesktopProject(args);
|
|
@@ -70,6 +95,26 @@ const main = defineCommand({
|
|
|
70
95
|
description: "Project template to clone from. File path pointing to a local tar file, npm package or a github repo",
|
|
71
96
|
valueHint: "template",
|
|
72
97
|
},
|
|
98
|
+
"template-ios": {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "iOS-specific template source",
|
|
101
|
+
valueHint: "template",
|
|
102
|
+
},
|
|
103
|
+
"template-android": {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "Android-specific template source",
|
|
106
|
+
valueHint: "template",
|
|
107
|
+
},
|
|
108
|
+
"template-macos": {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "macOS-specific template source",
|
|
111
|
+
valueHint: "template",
|
|
112
|
+
},
|
|
113
|
+
"template-windows": {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Windows-specific template source",
|
|
116
|
+
valueHint: "template",
|
|
117
|
+
},
|
|
73
118
|
platform: {
|
|
74
119
|
type: "string",
|
|
75
120
|
description: `Platforms to sync: macos, windows, desktop ${dim("(Default: desktop)")}`,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { IOSConfig } from "@expo/config-plugins";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Returns a list of files within a template matched by the resolved rename
|
|
7
|
+
* config.
|
|
8
|
+
*
|
|
9
|
+
* The rename config is resolved in the order of preference:
|
|
10
|
+
* Config provided as function param > defaultRenameConfig
|
|
11
|
+
*/
|
|
12
|
+
export async function getTemplateFilesToRenameAsync(cwd, { renameConfig: userConfig, } = {}) {
|
|
13
|
+
let config = userConfig ?? [...defaultRenameConfig];
|
|
14
|
+
// Strip comments, trim whitespace, and remove empty lines.
|
|
15
|
+
config = config
|
|
16
|
+
.map((line) => line.split(/(?<!\\)#/, 2)[0]?.trim() ?? "")
|
|
17
|
+
.filter((line) => line !== "");
|
|
18
|
+
return await glob(config, {
|
|
19
|
+
cwd,
|
|
20
|
+
// `true` is consistent with .gitignore. Allows `*.xml` to match .xml files
|
|
21
|
+
// in all subdirs.
|
|
22
|
+
matchBase: true,
|
|
23
|
+
dot: true,
|
|
24
|
+
// Prevent climbing out of the template directory in case a template
|
|
25
|
+
// includes a symlink to an external directory.
|
|
26
|
+
follow: false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* # Background
|
|
31
|
+
*
|
|
32
|
+
* `@expo/cli` and `create-expo` extract a template from a tarball (whether from
|
|
33
|
+
* a local npm project or a GitHub repository), but these templates have a
|
|
34
|
+
* static name that needs to be updated to match whatever app name the user
|
|
35
|
+
* specified.
|
|
36
|
+
*
|
|
37
|
+
* By convention, the app name of all templates is "HelloWorld". During
|
|
38
|
+
* extraction, filepaths are transformed via `createEntryResolver()` in
|
|
39
|
+
* `createFileTransform.ts`, but the contents of files are left untouched.
|
|
40
|
+
* Technically, the contents used to be transformed during extraction as well,
|
|
41
|
+
* but due to poor configurability, we've moved to a post-extraction approach.
|
|
42
|
+
*
|
|
43
|
+
* # The new approach: Renaming the app post-extraction
|
|
44
|
+
*
|
|
45
|
+
* In this new approach, we take a list of file patterns, otherwise known as the
|
|
46
|
+
* "rename config" to determine explicitly which files – relative to the root of
|
|
47
|
+
* the template – to perform find-and-replace on, to update the app name.
|
|
48
|
+
*
|
|
49
|
+
* ## The rename config
|
|
50
|
+
*
|
|
51
|
+
* The rename config can be passed directly as a string array to
|
|
52
|
+
* `getTemplateFilesToRenameAsync()`.
|
|
53
|
+
*
|
|
54
|
+
* The file patterns are formatted as glob expressions to be interpreted by
|
|
55
|
+
* [glob](https://github.com/isaacs/node-glob). Comments are supported with
|
|
56
|
+
* the `#` symbol, both in the plain-text file and string array formats.
|
|
57
|
+
* Whitespace is trimmed and whitespace-only lines are ignored.
|
|
58
|
+
*
|
|
59
|
+
* If no rename config has been passed directly to
|
|
60
|
+
* `getTemplateFilesToRenameAsync()` then this default rename config will be
|
|
61
|
+
* used instead.
|
|
62
|
+
*
|
|
63
|
+
* @see https://github.com/expo/expo/pull/27212
|
|
64
|
+
* @see https://github.com/expo/expo/blob/main/packages/%40expo/cli/src/prebuild/renameTemplateAppName.ts
|
|
65
|
+
*/
|
|
66
|
+
export const defaultRenameConfig = [
|
|
67
|
+
// Common
|
|
68
|
+
"!**/node_modules",
|
|
69
|
+
"app.json",
|
|
70
|
+
// Android
|
|
71
|
+
"android/**/*.gradle",
|
|
72
|
+
"android/app/BUCK",
|
|
73
|
+
"android/app/src/**/*.java",
|
|
74
|
+
"android/app/src/**/*.kt",
|
|
75
|
+
"android/app/src/**/*.xml",
|
|
76
|
+
// iOS
|
|
77
|
+
"ios/Podfile",
|
|
78
|
+
"ios/**/*.xcodeproj/project.pbxproj",
|
|
79
|
+
"ios/**/*.xcodeproj/xcshareddata/xcschemes/*.xcscheme",
|
|
80
|
+
"ios/**/*.xcworkspace/contents.xcworkspacedata",
|
|
81
|
+
// macOS
|
|
82
|
+
"macos/Podfile",
|
|
83
|
+
"macos/**/*.xcodeproj/project.pbxproj",
|
|
84
|
+
"macos/**/*.xcodeproj/xcshareddata/xcschemes/*.xcscheme",
|
|
85
|
+
"macos/**/*.xcworkspace/contents.xcworkspacedata",
|
|
86
|
+
// Windows
|
|
87
|
+
"windows/**/*.sln",
|
|
88
|
+
"windows/**/*.vcxproj",
|
|
89
|
+
"windows/**/*.vcxproj.filters",
|
|
90
|
+
"windows/**/*.vcxitems",
|
|
91
|
+
"windows/**/*.vcxitems.filters",
|
|
92
|
+
"windows/**/*.props",
|
|
93
|
+
"windows/**/*.targets",
|
|
94
|
+
"windows/**/*.h",
|
|
95
|
+
"windows/**/*.hpp",
|
|
96
|
+
"windows/**/*.c",
|
|
97
|
+
"windows/**/*.cpp",
|
|
98
|
+
"windows/**/*.idl",
|
|
99
|
+
"windows/**/*.rc",
|
|
100
|
+
"windows/**/*.xml",
|
|
101
|
+
"windows/**/*.xaml",
|
|
102
|
+
"windows/**/*.appxmanifest",
|
|
103
|
+
];
|
|
104
|
+
export async function renameTemplateAppNameAsync(cwd, { filesafeName, files, }) {
|
|
105
|
+
if (!files.length) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await Promise.all(files.map(async (file) => {
|
|
109
|
+
const absoluteFilePath = path.resolve(cwd, file);
|
|
110
|
+
let contents;
|
|
111
|
+
try {
|
|
112
|
+
contents = await fs.promises.readFile(absoluteFilePath, { encoding: "utf-8" });
|
|
113
|
+
}
|
|
114
|
+
catch (cause) {
|
|
115
|
+
throw new Error(`Failed to read template file: "${absoluteFilePath}". Was it removed mid-operation?`, { cause });
|
|
116
|
+
}
|
|
117
|
+
const safeName = [".xml", ".plist", ".xaml", ".appxmanifest"].includes(path.extname(file))
|
|
118
|
+
? escapeXMLCharacters(filesafeName)
|
|
119
|
+
: filesafeName;
|
|
120
|
+
try {
|
|
121
|
+
const replacement = contents
|
|
122
|
+
.replace(/Hello App Display Name/g, safeName)
|
|
123
|
+
.replace(/HelloWorld/g, IOSConfig.XcodeUtils.sanitizedName(safeName))
|
|
124
|
+
.replace(/helloworld/g, IOSConfig.XcodeUtils.sanitizedName(safeName.toLowerCase()));
|
|
125
|
+
if (replacement === contents) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await fs.promises.writeFile(absoluteFilePath, replacement);
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
throw new Error(`Failed to overwrite template file: "${absoluteFilePath}". Was it removed mid-operation?`, { cause });
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
function escapeXMLCharacters(original) {
|
|
136
|
+
const noAmps = original.replace("&", "&");
|
|
137
|
+
const noLt = noAmps.replace("<", "<");
|
|
138
|
+
const noGt = noLt.replace(">", ">");
|
|
139
|
+
const noApos = noGt.replace('"', '\\"');
|
|
140
|
+
return noApos.replace("'", "\\'");
|
|
141
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { tasks } from "@clack/prompts";
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { promisifiedSpawnTask } from "./child-process.js";
|
|
15
|
+
import { getTemplateFilesToRenameAsync, renameTemplateAppNameAsync, } from "./rename-template-app-name.js";
|
|
16
|
+
export async function applySelectedTemplatesAsync({ projectRoot, selection, enabledPlatforms, name, }) {
|
|
17
|
+
const descriptors = getOrderedTemplateDescriptors(selection, enabledPlatforms);
|
|
18
|
+
if (!descriptors.length) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Post-process the templates just like the `react-native-macos-init` and
|
|
22
|
+
// `react-native init-windows` commands do:
|
|
23
|
+
//
|
|
24
|
+
// macos:
|
|
25
|
+
// - https://github.com/microsoft/react-native-macos/blob/eb3bccb6e738650d617945770ec1319d5880084b/packages/react-native-macos-init/src/cli.ts#L398
|
|
26
|
+
// - https://github.com/microsoft/react-native-macos/blob/eb3bccb6e738650d617945770ec1319d5880084b/packages/react-native/local-cli/generate-macos.js#L18
|
|
27
|
+
// - https://github.com/microsoft/react-native-macos/tree/main/packages/react-native/local-cli/generator-macos/templates/macos
|
|
28
|
+
//
|
|
29
|
+
// windows:
|
|
30
|
+
// - https://github.com/microsoft/react-native-windows/blob/3d64f71ed8495da6a0dcfc1f97bcb8f761986594/packages/%40react-native-windows/cli/src/generator-windows/index.ts#L57
|
|
31
|
+
// - https://github.com/microsoft/react-native-windows/tree/main/vnext/templates/cpp-app
|
|
32
|
+
for (const descriptor of descriptors) {
|
|
33
|
+
const source = parseTemplateSource(descriptor.value);
|
|
34
|
+
const extracted = await prepareTemplateSourceAsync(source);
|
|
35
|
+
try {
|
|
36
|
+
const templateRoot = await resolveTemplateRootAsync(extracted, source);
|
|
37
|
+
const templateConfig = await loadTemplateConfigAsync(templateRoot);
|
|
38
|
+
await copyTemplateFilesAsync({
|
|
39
|
+
sourceRoot: templateRoot,
|
|
40
|
+
projectRoot,
|
|
41
|
+
name,
|
|
42
|
+
templateConfig,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await fs.rm(extracted, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getOrderedTemplateDescriptors(selection, enabledPlatforms) {
|
|
51
|
+
const platformSet = new Set(enabledPlatforms);
|
|
52
|
+
const descriptors = new Array();
|
|
53
|
+
if (selection.template) {
|
|
54
|
+
descriptors.push({ key: "template", value: selection.template });
|
|
55
|
+
}
|
|
56
|
+
if (selection["template-ios"] && platformSet.has("ios")) {
|
|
57
|
+
descriptors.push({
|
|
58
|
+
key: "template-ios",
|
|
59
|
+
value: selection["template-ios"],
|
|
60
|
+
forPlatform: "ios",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (selection["template-android"] && platformSet.has("android")) {
|
|
64
|
+
descriptors.push({
|
|
65
|
+
key: "template-android",
|
|
66
|
+
value: selection["template-android"],
|
|
67
|
+
forPlatform: "android",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (selection["template-macos"] && platformSet.has("macos")) {
|
|
71
|
+
descriptors.push({
|
|
72
|
+
key: "template-macos",
|
|
73
|
+
value: selection["template-macos"],
|
|
74
|
+
forPlatform: "macos",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (selection["template-windows"] && platformSet.has("windows")) {
|
|
78
|
+
descriptors.push({
|
|
79
|
+
key: "template-windows",
|
|
80
|
+
value: selection["template-windows"],
|
|
81
|
+
forPlatform: "windows",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return descriptors;
|
|
85
|
+
}
|
|
86
|
+
function parseTemplateSource(template) {
|
|
87
|
+
const localPath = path.resolve(process.cwd(), template);
|
|
88
|
+
if (/\.(?:tar|tgz|tar\.gz)$/i.test(template)) {
|
|
89
|
+
return { type: "local-tarball", path: localPath };
|
|
90
|
+
}
|
|
91
|
+
const githubUrlMatch = template.match(/^https:\/\/github\.com\/([^/]+)\/([^/#]+?)(?:\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?$/);
|
|
92
|
+
if (githubUrlMatch) {
|
|
93
|
+
const [, owner, repo, ref = "HEAD", subpath] = githubUrlMatch;
|
|
94
|
+
return { type: "github", owner, repo, ref, subpath: subpath ?? null };
|
|
95
|
+
}
|
|
96
|
+
const githubShorthandMatch = template.match(/^([^/\s#]+)\/([^/\s#]+)(?:#(.+))?$/);
|
|
97
|
+
if (githubShorthandMatch) {
|
|
98
|
+
const [, owner, repo, rawRef] = githubShorthandMatch;
|
|
99
|
+
const [ref, ...subpathParts] = (rawRef ?? "HEAD").split(":");
|
|
100
|
+
return {
|
|
101
|
+
type: "github",
|
|
102
|
+
owner,
|
|
103
|
+
repo,
|
|
104
|
+
ref,
|
|
105
|
+
subpath: subpathParts.length ? subpathParts.join(":") : null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { type: "npm", spec: template };
|
|
109
|
+
}
|
|
110
|
+
async function prepareTemplateSourceAsync(source) {
|
|
111
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "expo-desktop-template-"));
|
|
112
|
+
const archivePath = path.join(tempRoot, "template.tgz");
|
|
113
|
+
switch (source.type) {
|
|
114
|
+
case "local-tarball":
|
|
115
|
+
await fs.copyFile(source.path, archivePath);
|
|
116
|
+
break;
|
|
117
|
+
case "github": {
|
|
118
|
+
const tarballUrl = `https://codeload.github.com/${source.owner}/${source.repo}/tar.gz/${source.ref}`;
|
|
119
|
+
const response = await fetch(tarballUrl);
|
|
120
|
+
if (!response.ok || !response.body) {
|
|
121
|
+
throw new Error(`Failed to download template tarball from ${tarballUrl}`);
|
|
122
|
+
}
|
|
123
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
124
|
+
await fs.writeFile(archivePath, bytes);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "npm": {
|
|
128
|
+
await tasks([
|
|
129
|
+
promisifiedSpawnTask({
|
|
130
|
+
title: `npm pack (${source.spec})`,
|
|
131
|
+
command: "npm",
|
|
132
|
+
args: ["pack", source.spec, "--silent"],
|
|
133
|
+
options: { cwd: tempRoot },
|
|
134
|
+
}),
|
|
135
|
+
]);
|
|
136
|
+
const entries = await fs.readdir(tempRoot);
|
|
137
|
+
const packed = entries.find((entry) => entry.endsWith(".tgz"));
|
|
138
|
+
if (!packed) {
|
|
139
|
+
throw new Error(`Could not pack template "${source.spec}".`);
|
|
140
|
+
}
|
|
141
|
+
await fs.rename(path.join(tempRoot, packed), archivePath);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await tasks([
|
|
146
|
+
promisifiedSpawnTask({
|
|
147
|
+
title: "extracting template",
|
|
148
|
+
command: "tar",
|
|
149
|
+
args: ["-xzf", archivePath, "-C", tempRoot],
|
|
150
|
+
}),
|
|
151
|
+
]);
|
|
152
|
+
return tempRoot;
|
|
153
|
+
}
|
|
154
|
+
async function resolveTemplateRootAsync(extractedRoot, source) {
|
|
155
|
+
const entries = await fs.readdir(extractedRoot, { withFileTypes: true });
|
|
156
|
+
const firstDir = entries.find((entry) => entry.isDirectory() && entry.name !== ".git");
|
|
157
|
+
if (!firstDir) {
|
|
158
|
+
throw new Error("Extracted template archive did not contain a root directory.");
|
|
159
|
+
}
|
|
160
|
+
let templateRoot = path.join(extractedRoot, firstDir.name);
|
|
161
|
+
if (source.type === "npm") {
|
|
162
|
+
templateRoot = path.join(templateRoot, "package");
|
|
163
|
+
}
|
|
164
|
+
if (source.type === "github" && source.subpath) {
|
|
165
|
+
templateRoot = path.join(templateRoot, source.subpath);
|
|
166
|
+
}
|
|
167
|
+
return templateRoot;
|
|
168
|
+
}
|
|
169
|
+
async function loadTemplateConfigAsync(templateRoot) {
|
|
170
|
+
const configPath = path.join(templateRoot, "template.config.js");
|
|
171
|
+
try {
|
|
172
|
+
await fs.access(configPath);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const imported = (await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href)));
|
|
178
|
+
return imported.default ?? imported;
|
|
179
|
+
}
|
|
180
|
+
async function copyTemplateFilesAsync({ sourceRoot, projectRoot, name, templateConfig, }) {
|
|
181
|
+
const mappings = templateConfig?.files?.length
|
|
182
|
+
? templateConfig.files.map((mapping) => ({
|
|
183
|
+
from: path.join(sourceRoot, mapping.from),
|
|
184
|
+
to: mapping.to ?? mapping.from,
|
|
185
|
+
}))
|
|
186
|
+
: await discoverAllFilesAsync(sourceRoot);
|
|
187
|
+
const pathReplacements = {
|
|
188
|
+
HelloWorld: name.filesafeName,
|
|
189
|
+
helloworld: name.filesafeName.toLowerCase(),
|
|
190
|
+
...(templateConfig?.pathReplacements ?? {}),
|
|
191
|
+
};
|
|
192
|
+
const copiedRelativePaths = new Array();
|
|
193
|
+
for (const mapping of mappings) {
|
|
194
|
+
const relativePath = replaceTokens(mapping.to, pathReplacements);
|
|
195
|
+
const targetPath = path.join(projectRoot, relativePath);
|
|
196
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
197
|
+
await fs.copyFile(mapping.from, targetPath);
|
|
198
|
+
copiedRelativePaths.push(relativePath);
|
|
199
|
+
}
|
|
200
|
+
if (templateConfig?.replacements && Object.keys(templateConfig.replacements).length > 0) {
|
|
201
|
+
await applyExtraReplacementsAsync({
|
|
202
|
+
cwd: projectRoot,
|
|
203
|
+
files: copiedRelativePaths,
|
|
204
|
+
replacements: templateConfig.replacements,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const filesFromRenameConfig = await getTemplateFilesToRenameAsync(projectRoot, {
|
|
208
|
+
renameConfig: templateConfig?.renameConfig,
|
|
209
|
+
});
|
|
210
|
+
const copiedSet = new Set(copiedRelativePaths.map(normalizeToPosixPath));
|
|
211
|
+
const filesToRename = filesFromRenameConfig.filter((file) => copiedSet.has(normalizeToPosixPath(file)));
|
|
212
|
+
await renameTemplateAppNameAsync(projectRoot, {
|
|
213
|
+
filesafeName: name.filesafeName,
|
|
214
|
+
files: filesToRename,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async function discoverAllFilesAsync(sourceRoot) {
|
|
218
|
+
const out = new Array();
|
|
219
|
+
await walkFilesAsync(sourceRoot, sourceRoot, out);
|
|
220
|
+
return out.filter((entry) => path.basename(entry.to) !== "template.config.js");
|
|
221
|
+
}
|
|
222
|
+
async function walkFilesAsync(currentDir, sourceRoot, output) {
|
|
223
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
const absolute = path.join(currentDir, entry.name);
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
await walkFilesAsync(absolute, sourceRoot, output);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!entry.isFile()) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
output.push({ from: absolute, to: path.relative(sourceRoot, absolute) });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function replaceTokens(input, replacements) {
|
|
237
|
+
let output = input;
|
|
238
|
+
for (const [from, to] of Object.entries(replacements)) {
|
|
239
|
+
output = output.split(from).join(to);
|
|
240
|
+
}
|
|
241
|
+
return output;
|
|
242
|
+
}
|
|
243
|
+
async function applyExtraReplacementsAsync({ cwd, files, replacements, }) {
|
|
244
|
+
for (const file of files) {
|
|
245
|
+
const absolute = path.join(cwd, file);
|
|
246
|
+
let contents;
|
|
247
|
+
try {
|
|
248
|
+
contents = await fs.readFile(absolute, "utf8");
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const replacement = replaceTokens(contents, replacements);
|
|
257
|
+
if (replacement === contents) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
await fs.writeFile(absolute, replacement, "utf8");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function normalizeToPosixPath(input) {
|
|
264
|
+
return input.replaceAll(path.sep, "/");
|
|
265
|
+
}
|
|
@@ -17,6 +17,13 @@ export async function newExpoDesktopProject(args) {
|
|
|
17
17
|
rdns: "com.example.my-app-123",
|
|
18
18
|
},
|
|
19
19
|
packageManager: "bun",
|
|
20
|
+
templates: {
|
|
21
|
+
template: args.template,
|
|
22
|
+
"template-ios": args["template-ios"],
|
|
23
|
+
"template-android": args["template-android"],
|
|
24
|
+
"template-macos": args["template-macos"],
|
|
25
|
+
"template-windows": args["template-windows"],
|
|
26
|
+
},
|
|
20
27
|
versions: {
|
|
21
28
|
expoMajor: 54,
|
|
22
29
|
expoBlankTypeScript: "54.0.45",
|
|
@@ -49,7 +56,18 @@ export async function newExpoDesktopProject(args) {
|
|
|
49
56
|
if (isCancel(packageManager)) {
|
|
50
57
|
process.exit(0);
|
|
51
58
|
}
|
|
52
|
-
await createExpoDesktopApp({
|
|
59
|
+
await createExpoDesktopApp({
|
|
60
|
+
name,
|
|
61
|
+
packageManager,
|
|
62
|
+
versions,
|
|
63
|
+
templates: {
|
|
64
|
+
template: args.template,
|
|
65
|
+
"template-ios": args["template-ios"],
|
|
66
|
+
"template-android": args["template-android"],
|
|
67
|
+
"template-macos": args["template-macos"],
|
|
68
|
+
"template-windows": args["template-windows"],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
53
71
|
}
|
|
54
72
|
async function configureAppName(args) {
|
|
55
73
|
const { initialFilesafeName, initialDisplayName, initialRdns } = args;
|
|
@@ -11,6 +11,7 @@ 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 { applySelectedTemplatesAsync } from "../common/template.js";
|
|
14
15
|
/**
|
|
15
16
|
* A crude switch to use to help with local development.
|
|
16
17
|
*
|
|
@@ -20,9 +21,29 @@ import { packageManagerExec } from "../common/npm.js";
|
|
|
20
21
|
* - Adds the apply-config-plugins.mjs script.
|
|
21
22
|
*/
|
|
22
23
|
export const localDev = false;
|
|
23
|
-
export async function createExpoDesktopApp({ name, packageManager, versions, }) {
|
|
24
|
+
export async function createExpoDesktopApp({ name, packageManager, templates, versions, }) {
|
|
24
25
|
const { projectPath } = await createExpoApp({ name, packageManager, versions });
|
|
25
26
|
await appendRootGitignoreSpawnDebugLogs(projectPath);
|
|
27
|
+
const templateSelection = {
|
|
28
|
+
// https://github.com/expo/expo/blob/sdk-54/templates/expo-template-blank-typescript
|
|
29
|
+
template: templates.template,
|
|
30
|
+
"template-ios": templates["template-ios"],
|
|
31
|
+
"template-android": templates["template-android"],
|
|
32
|
+
// https://github.com/microsoft/react-native-macos/tree/main/packages/react-native/local-cli/generator-macos/templates/macos
|
|
33
|
+
"template-macos": templates["template-macos"] ??
|
|
34
|
+
"microsoft/react-native-macos#main:packages/react-native/local-cli/generator-macos/templates",
|
|
35
|
+
// https://github.com/microsoft/react-native-windows/tree/main/vnext/templates/cpp-app
|
|
36
|
+
"template-windows": templates["template-windows"] ??
|
|
37
|
+
"microsoft/react-native-windows#main:vnext/templates/cpp-app",
|
|
38
|
+
};
|
|
39
|
+
title("Applying templates…", { spacing: 1 });
|
|
40
|
+
await applySelectedTemplatesAsync({
|
|
41
|
+
projectRoot: projectPath,
|
|
42
|
+
selection: templateSelection,
|
|
43
|
+
enabledPlatforms: ["ios", "android", "macos", "windows"],
|
|
44
|
+
name,
|
|
45
|
+
});
|
|
46
|
+
console.log(`${green("◆")} Applied templates.\n`);
|
|
26
47
|
title("Altering app.json…", { spacing: 1 });
|
|
27
48
|
await updateAppJson({ name, projectPath });
|
|
28
49
|
title("Altering package.json…", { spacing: 1 });
|
|
@@ -34,11 +55,6 @@ export async function createExpoDesktopApp({ name, packageManager, versions, })
|
|
|
34
55
|
});
|
|
35
56
|
title("Installing dependencies…", { spacing: 1 });
|
|
36
57
|
await npmInstall({ cwd: projectPath, packageManager });
|
|
37
|
-
title("Adding the Windows app…", { spacing: 1 });
|
|
38
|
-
await addDesktopApp({ cwd: projectPath, name, packageManager, type: "windows", versions });
|
|
39
|
-
title("Adding the macOS app…", { spacing: 1 });
|
|
40
|
-
await updatePackageJson({ name, projectPath, versions, task: { type: "pre-init-macos" } });
|
|
41
|
-
await addDesktopApp({ cwd: projectPath, name, packageManager, type: "macos", versions });
|
|
42
58
|
await updatePackageJson({
|
|
43
59
|
name,
|
|
44
60
|
projectPath,
|
|
@@ -201,23 +217,7 @@ async function updatePackageJson({ name, projectPath, task, versions, }) {
|
|
|
201
217
|
throw new Error(`Invalid config:\n${makePrettySummary(packageJson).join("\n")}`);
|
|
202
218
|
}
|
|
203
219
|
const nameBefore = packageJson.name;
|
|
204
|
-
if (task.type === "
|
|
205
|
-
// create-expo-app shifts this to lowercase as per package.json rules, and
|
|
206
|
-
// then react-native-macos-init maddeningly uses it in preference over the
|
|
207
|
-
// app.json "name" value.
|
|
208
|
-
// https://github.com/microsoft/react-native-macos/blob/eb3bccb6e738650d617945770ec1319d5880084b/packages/react-native-macos-init/src/cli.ts#L74-L75
|
|
209
|
-
//
|
|
210
|
-
// If only the underlying generateMacOS() / copyProjectTemplateAndReplace()
|
|
211
|
-
// were exposed, we could just pass the name needed.
|
|
212
|
-
// https://github.com/microsoft/react-native-macos/blob/eb3bccb6e738650d617945770ec1319d5880084b/packages/react-native-macos-init/src/cli.ts#L398
|
|
213
|
-
// https://github.com/microsoft/react-native-macos/blob/eb3bccb6e738650d617945770ec1319d5880084b/packages/react-native/local-cli/generate-macos.js#L18
|
|
214
|
-
//
|
|
215
|
-
// But as it's not, our best option is to just write an invalid name into
|
|
216
|
-
// the package.json temporarily (or remove it altogether). We'll set it
|
|
217
|
-
// back to lower case later in the "restore-name" task.
|
|
218
|
-
packageJson.name = name.filesafeName;
|
|
219
|
-
}
|
|
220
|
-
else if (task.type === "post-init-macos") {
|
|
220
|
+
if (task.type === "post-init-macos") {
|
|
221
221
|
if (task.name) {
|
|
222
222
|
packageJson.name = task.name;
|
|
223
223
|
}
|
|
@@ -295,36 +295,6 @@ async function npmInstall({ cwd, packageManager, }) {
|
|
|
295
295
|
}
|
|
296
296
|
console.log(`\n${green("◆")} Installed dependencies.\n`);
|
|
297
297
|
}
|
|
298
|
-
async function addDesktopApp({ name, packageManager, versions, type, cwd, }) {
|
|
299
|
-
const { args, command } = packageManagerExec(packageManager);
|
|
300
|
-
switch (type) {
|
|
301
|
-
case "macos":
|
|
302
|
-
args.push("react-native-macos-init", "--version", versions.macos);
|
|
303
|
-
break;
|
|
304
|
-
case "windows":
|
|
305
|
-
args.push("react-native", "init-windows", "--template", "cpp-app", "--namespace", name.rdns.replaceAll(/[-_]/g, ""), "--name", name.filesafeName);
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
const printedCommand = `${command} ${args.join(" ")}`;
|
|
309
|
-
console.log(`${cyan("◆")} Running: ${yellow(printedCommand)}\n`);
|
|
310
|
-
try {
|
|
311
|
-
await tasks([
|
|
312
|
-
promisifiedSpawnTask({
|
|
313
|
-
title: type === "macos"
|
|
314
|
-
? "react-native-macos-init"
|
|
315
|
-
: `react-native init-windows (${name.filesafeName})`,
|
|
316
|
-
command,
|
|
317
|
-
args,
|
|
318
|
-
options: { cwd, stdio: "inherit" },
|
|
319
|
-
}),
|
|
320
|
-
]);
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
log.error(`Error running ${yellow(printedCommand)}${error instanceof Error ? `: ${error.message}` : "."}`);
|
|
324
|
-
process.exit(1);
|
|
325
|
-
}
|
|
326
|
-
console.log(`${green("◆")} Added ${yellow(type)} app.\n`);
|
|
327
|
-
}
|
|
328
298
|
async function runPrebuildMobile({ packageManager, projectPath, }) {
|
|
329
299
|
const { args, command } = packageManagerExec(packageManager);
|
|
330
300
|
args.push("expo", "prebuild", "--no-install");
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { log } from "@clack/prompts";
|
|
2
|
+
import { type } from "arktype";
|
|
2
3
|
import { default as kleur } from "kleur";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
3
6
|
import { exit } from "node:process";
|
|
7
|
+
import { AppJson } from "../common/app-json.js";
|
|
8
|
+
import { applySelectedTemplatesAsync } from "../common/template.js";
|
|
4
9
|
import { resolvePackageManagerOptions } from "./resolve-options.js";
|
|
5
10
|
/**
|
|
6
11
|
* The entrypoint for `npx expo prebuild` is here:
|
|
@@ -12,21 +17,10 @@ import { resolvePackageManagerOptions } from "./resolve-options.js";
|
|
|
12
17
|
* @see https://github.com/expo/expo/blob/15d35298c9a397c23bcbf6b20e2b9761564acbc4/packages/%40expo/cli/src/prebuild/index.ts#L7
|
|
13
18
|
* @see https://github.com/expo/expo/blob/15d35298c9a397c23bcbf6b20e2b9761564acbc4/packages/%40expo/cli/src/prebuild/configureProjectAsync.ts#L37
|
|
14
19
|
*/
|
|
15
|
-
export async function prebuild({ clean, "no-install": noInstall, npm, yarn, bun, pnpm, template, platform, }) {
|
|
20
|
+
export async function prebuild({ clean, "no-install": noInstall, npm, yarn, bun, pnpm, template, "template-ios": templateIos, "template-android": templateAndroid, "template-macos": templateMacos, "template-windows": templateWindows, platform, }) {
|
|
16
21
|
log.info(`🏎️ Running ${kleur.yellow("expo-desktop prebuild")}.`, { withGuide: false });
|
|
17
22
|
// TODO: if packageManager undefined, infer from lockfiles
|
|
18
23
|
const _packageManager = resolvePackageManagerOptions({ noInstall, npm, yarn, bun, pnpm });
|
|
19
|
-
if (template) {
|
|
20
|
-
// macos:
|
|
21
|
-
// - https://github.com/microsoft/react-native-macos/blob/eb3bccb6e738650d617945770ec1319d5880084b/packages/react-native-macos-init/src/cli.ts#L398
|
|
22
|
-
// - https://github.com/microsoft/react-native-macos/blob/eb3bccb6e738650d617945770ec1319d5880084b/packages/react-native/local-cli/generate-macos.js#L18
|
|
23
|
-
// - https://github.com/microsoft/react-native-macos/tree/main/packages/react-native/local-cli/generator-macos/templates/macos
|
|
24
|
-
//
|
|
25
|
-
// windows:
|
|
26
|
-
// - https://github.com/microsoft/react-native-windows/blob/3d64f71ed8495da6a0dcfc1f97bcb8f761986594/packages/%40react-native-windows/cli/src/generator-windows/index.ts#L57
|
|
27
|
-
// - https://github.com/microsoft/react-native-windows/tree/main/vnext/templates/cpp-app
|
|
28
|
-
throw new Error("--template arg not yet implemented.");
|
|
29
|
-
}
|
|
30
24
|
if (platform !== "macos" &&
|
|
31
25
|
platform !== "windows" &&
|
|
32
26
|
platform !== "desktop" &&
|
|
@@ -43,6 +37,24 @@ export async function prebuild({ clean, "no-install": noInstall, npm, yarn, bun,
|
|
|
43
37
|
if (!platforms.length) {
|
|
44
38
|
throw new Error("At least one platform must be enabled when syncing");
|
|
45
39
|
}
|
|
40
|
+
const templateSelection = {
|
|
41
|
+
template,
|
|
42
|
+
"template-ios": templateIos,
|
|
43
|
+
"template-android": templateAndroid,
|
|
44
|
+
"template-macos": templateMacos,
|
|
45
|
+
"template-windows": templateWindows,
|
|
46
|
+
};
|
|
47
|
+
if (clean && hasTemplateSelection(templateSelection)) {
|
|
48
|
+
const projectRoot = process.cwd();
|
|
49
|
+
const appName = await readAppNameFromConfigAsync(projectRoot);
|
|
50
|
+
await applySelectedTemplatesAsync({
|
|
51
|
+
projectRoot,
|
|
52
|
+
selection: templateSelection,
|
|
53
|
+
enabledPlatforms: platforms,
|
|
54
|
+
name: appName,
|
|
55
|
+
});
|
|
56
|
+
log.info("Applied project templates for clean prebuild.", { withGuide: false });
|
|
57
|
+
}
|
|
46
58
|
// TODO:
|
|
47
59
|
// - prebuildAsync()
|
|
48
60
|
// - https://github.com/expo/expo/blob/8dd645080f52927e2a8bf406167da7241a1d46d8/packages/%40expo/cli/src/prebuild/prebuildAsync.ts#L49
|
|
@@ -67,3 +79,26 @@ export async function prebuild({ clean, "no-install": noInstall, npm, yarn, bun,
|
|
|
67
79
|
log.error(`${kleur.yellow("expo-desktop prebuild")} not yet implemented.`);
|
|
68
80
|
return exit(1);
|
|
69
81
|
}
|
|
82
|
+
function hasTemplateSelection(selection) {
|
|
83
|
+
return Boolean(selection.template ||
|
|
84
|
+
selection["template-ios"] ||
|
|
85
|
+
selection["template-android"] ||
|
|
86
|
+
selection["template-macos"] ||
|
|
87
|
+
selection["template-windows"]);
|
|
88
|
+
}
|
|
89
|
+
async function readAppNameFromConfigAsync(projectRoot) {
|
|
90
|
+
const appJsonPath = path.join(projectRoot, "app.json");
|
|
91
|
+
const contents = await fs.readFile(appJsonPath, "utf8");
|
|
92
|
+
const parsed = AppJson(JSON.parse(contents));
|
|
93
|
+
if (parsed instanceof type.errors) {
|
|
94
|
+
throw new Error("Invalid app.json while resolving template replacements.");
|
|
95
|
+
}
|
|
96
|
+
const filesafeName = parsed.expo?.name ?? "HelloWorld";
|
|
97
|
+
const displayName = parsed.expo?.name ?? filesafeName;
|
|
98
|
+
const rdns = parsed.expo?.ios?.bundleIdentifier ?? parsed.expo?.android?.package ?? "com.helloworld";
|
|
99
|
+
return {
|
|
100
|
+
filesafeName,
|
|
101
|
+
displayName,
|
|
102
|
+
rdns,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-desktop",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Best-effort desktop support for Expo",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
"@clack/prompts": "^1.2.0",
|
|
38
38
|
"arktype": "^2.2.0",
|
|
39
39
|
"citty": "^0.2.2",
|
|
40
|
+
"glob": "^10.5.0",
|
|
40
41
|
"kleur": "^4.1.5",
|
|
41
42
|
"toml": "^4.1.1",
|
|
42
|
-
"expo-desktop-config-plugins": "1.1.
|
|
43
|
-
"expo-desktop-prebuild-config": "1.0.
|
|
43
|
+
"expo-desktop-config-plugins": "1.1.17",
|
|
44
|
+
"expo-desktop-prebuild-config": "1.0.6"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@expo/config": "^12.0.13",
|