expo-desktop 0.1.16 → 0.1.18
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/common/apply-windows-cpp-app-template.js +369 -0
- package/build/common/rename-template-app-name.js +3 -1
- package/build/common/template.js +41 -8
- package/build/create-app/command.js +3 -3
- package/build/create-app/create-expo-desktop-app.js +6 -0
- package/build/prebuild/command.js +1 -0
- package/package.json +5 -3
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
import mustache from "mustache";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
8
|
+
/**
|
|
9
|
+
* Mirrors the effective behaviour of `vnext/templates/cpp-app/template.config.js`
|
|
10
|
+
* from react-native-windows (path renames + Mustache replacements) without
|
|
11
|
+
* executing that file (it pulls undeclared deps and `../templateUtils`).
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/microsoft/react-native-windows/blob/main/vnext/templates/cpp-app/template.config.js
|
|
14
|
+
* @see https://github.com/microsoft/react-native-windows/blob/main/packages/%40react-native-windows/cli/src/generator-common/index.ts
|
|
15
|
+
*/
|
|
16
|
+
export async function applyWindowsCppAppTemplateAsync(projectRoot, name) {
|
|
17
|
+
const windowsRoot = path.join(projectRoot, "windows");
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(windowsRoot);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!(await looksLikeCppAppTemplateAsync(windowsRoot))) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const replacements = await buildReplacementsRecord(projectRoot, name);
|
|
31
|
+
await renameCppAppPathsAsync(windowsRoot, name.filesafeName);
|
|
32
|
+
await renderMustacheUnderWindowsAsync(windowsRoot, replacements);
|
|
33
|
+
await finalizeWindowsCppTemplateArtifacts(projectRoot, windowsRoot);
|
|
34
|
+
}
|
|
35
|
+
async function looksLikeCppAppTemplateAsync(windowsRoot) {
|
|
36
|
+
try {
|
|
37
|
+
await fs.access(path.join(windowsRoot, "MyApp"));
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const entries = await fs.readdir(windowsRoot).catch(() => []);
|
|
45
|
+
return entries.some((e) => e.includes("MyApp"));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Same as legacy `react-native init-windows --namespace` (strip `-` and `_` only). */
|
|
49
|
+
function windowsNamespaceFromRdns(rdns) {
|
|
50
|
+
return rdns.replaceAll(/[-_]/g, "");
|
|
51
|
+
}
|
|
52
|
+
async function tryReadRnwFromNodeModules(projectRoot) {
|
|
53
|
+
const pkgPath = path.join(projectRoot, "node_modules", "react-native-windows", "package.json");
|
|
54
|
+
try {
|
|
55
|
+
const raw = await fs.readFile(pkgPath, "utf8");
|
|
56
|
+
const version = JSON.parse(raw).version;
|
|
57
|
+
if (!version) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const rnwPath = path.dirname(pkgPath);
|
|
61
|
+
let devMode = false;
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(path.join(rnwPath, "src-win"));
|
|
64
|
+
devMode = true;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
devMode = false;
|
|
71
|
+
}
|
|
72
|
+
return { path: rnwPath, version, devMode };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function reactNativeWindowsVersionFromPackageJson(projectRoot) {
|
|
82
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
83
|
+
try {
|
|
84
|
+
const raw = await fs.readFile(pkgPath, "utf8");
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
const v = parsed.dependencies?.["react-native-windows"];
|
|
87
|
+
return typeof v === "string" ? v : null;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function buildReplacementsRecord(projectRoot, name) {
|
|
97
|
+
const projectGuid = crypto.randomUUID();
|
|
98
|
+
const packageGuid = crypto.randomUUID();
|
|
99
|
+
const namespace = windowsNamespaceFromRdns(name.rdns);
|
|
100
|
+
const namespaceCpp = namespace.replaceAll(".", "::");
|
|
101
|
+
const mainComponentName = name.filesafeName;
|
|
102
|
+
const rnw = await tryReadRnwFromNodeModules(projectRoot);
|
|
103
|
+
const rnwVersion = rnw?.version ?? (await reactNativeWindowsVersionFromPackageJson(projectRoot)) ?? "0.0.0";
|
|
104
|
+
const rnwPathFromProjectRoot = rnw
|
|
105
|
+
? path.relative(projectRoot, rnw.path).replaceAll("/", "\\")
|
|
106
|
+
: "node_modules\\react-native-windows";
|
|
107
|
+
const devMode = rnw?.devMode ?? false;
|
|
108
|
+
const isCanary = rnwVersion.includes("canary");
|
|
109
|
+
return {
|
|
110
|
+
name: name.filesafeName,
|
|
111
|
+
namespace,
|
|
112
|
+
namespaceCpp,
|
|
113
|
+
rnwVersion,
|
|
114
|
+
rnwPathFromProjectRoot,
|
|
115
|
+
mainComponentName,
|
|
116
|
+
projectGuidLower: `{${projectGuid.toLowerCase()}}`,
|
|
117
|
+
projectGuidUpper: `{${projectGuid.toUpperCase()}}`,
|
|
118
|
+
packageGuidLower: `{${packageGuid.toLowerCase()}}`,
|
|
119
|
+
packageGuidUpper: `{${packageGuid.toUpperCase()}}`,
|
|
120
|
+
currentUser: os.userInfo().username,
|
|
121
|
+
devMode,
|
|
122
|
+
useNuGets: !devMode,
|
|
123
|
+
addReactNativePublicAdoFeed: true || isCanary,
|
|
124
|
+
cppNugetPackages: [],
|
|
125
|
+
autolinkPropertiesForProps: "",
|
|
126
|
+
autolinkProjectReferencesForTargets: "",
|
|
127
|
+
autolinkCppIncludes: "",
|
|
128
|
+
autolinkCppPackageProviders: "\n UNREFERENCED_PARAMETER(packageProviders);",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function cppAppRelativePathTransform(relativeToWindows, filesafeName) {
|
|
132
|
+
const parts = relativeToWindows.split(/[/\\]/);
|
|
133
|
+
const base = parts.at(-1) ?? "";
|
|
134
|
+
if (base === "_gitignore") {
|
|
135
|
+
parts[parts.length - 1] = ".gitignore";
|
|
136
|
+
}
|
|
137
|
+
else if (base === "NuGet_Config") {
|
|
138
|
+
parts[parts.length - 1] = "NuGet.config";
|
|
139
|
+
}
|
|
140
|
+
return parts.join(path.sep).split("MyApp").join(filesafeName);
|
|
141
|
+
}
|
|
142
|
+
function pathDepth(p) {
|
|
143
|
+
return p.split(path.sep).filter(Boolean).length;
|
|
144
|
+
}
|
|
145
|
+
async function collectWindowsPathsAsync(windowsRoot) {
|
|
146
|
+
const out = [];
|
|
147
|
+
async function walk(current) {
|
|
148
|
+
let stat;
|
|
149
|
+
try {
|
|
150
|
+
stat = await fs.lstat(current);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (stat.isSymbolicLink()) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (stat.isDirectory()) {
|
|
162
|
+
out.push(current);
|
|
163
|
+
for (const ent of await fs.readdir(current, { withFileTypes: true })) {
|
|
164
|
+
await walk(path.join(current, ent.name));
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (stat.isFile()) {
|
|
169
|
+
out.push(current);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await walk(windowsRoot);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Renames cpp-app paths containing `MyApp` → filesafeName. Uses iterative
|
|
177
|
+
* shallow-first ordering (directories before files at the same depth). Deeper
|
|
178
|
+
* fixes caused ENOTEMPTY: files were renamed into `*.Package/Images` before the
|
|
179
|
+
* directory `MyApp.Package/Images` moved, leaving a non-empty destination.
|
|
180
|
+
*/
|
|
181
|
+
async function renameCppAppPathsAsync(windowsRoot, filesafeName) {
|
|
182
|
+
for (;;) {
|
|
183
|
+
const allPaths = await collectWindowsPathsAsync(windowsRoot);
|
|
184
|
+
const candidates = [];
|
|
185
|
+
for (const abs of allPaths) {
|
|
186
|
+
const rel = path.relative(windowsRoot, abs);
|
|
187
|
+
if (!rel || rel.includes("..")) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!rel.includes("MyApp")) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
let stat;
|
|
194
|
+
try {
|
|
195
|
+
stat = await fs.lstat(abs);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (stat.isSymbolicLink()) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
candidates.push({
|
|
207
|
+
abs,
|
|
208
|
+
rel,
|
|
209
|
+
depth: pathDepth(rel),
|
|
210
|
+
isDir: stat.isDirectory(),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (!candidates.length) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
candidates.sort((a, b) => {
|
|
217
|
+
if (a.depth !== b.depth) {
|
|
218
|
+
return a.depth - b.depth;
|
|
219
|
+
}
|
|
220
|
+
if (a.isDir !== b.isDir) {
|
|
221
|
+
return a.isDir ? -1 : 1;
|
|
222
|
+
}
|
|
223
|
+
return a.rel.localeCompare(b.rel);
|
|
224
|
+
});
|
|
225
|
+
const chosen = candidates[0];
|
|
226
|
+
const newRelWin = cppAppRelativePathTransform(chosen.rel, filesafeName);
|
|
227
|
+
if (newRelWin === chosen.rel) {
|
|
228
|
+
throw new Error(`Windows cpp-app rename: expected path containing MyApp to change: "${chosen.rel}"`);
|
|
229
|
+
}
|
|
230
|
+
const newAbs = path.join(windowsRoot, newRelWin);
|
|
231
|
+
if (newAbs === chosen.abs) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
await fs.mkdir(path.dirname(newAbs), { recursive: true });
|
|
235
|
+
try {
|
|
236
|
+
await fs.rename(chosen.abs, newAbs);
|
|
237
|
+
}
|
|
238
|
+
catch (cause) {
|
|
239
|
+
throw new Error(`Failed to rename "${chosen.abs}" -> "${newAbs}"`, { cause });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const BINARY_EXTENSIONS = new Set([
|
|
244
|
+
".png",
|
|
245
|
+
".jpg",
|
|
246
|
+
".jpeg",
|
|
247
|
+
".gif",
|
|
248
|
+
".webp",
|
|
249
|
+
".jar",
|
|
250
|
+
".keystore",
|
|
251
|
+
".ico",
|
|
252
|
+
".pdb",
|
|
253
|
+
".dll",
|
|
254
|
+
".exe",
|
|
255
|
+
".bin",
|
|
256
|
+
]);
|
|
257
|
+
function isProbablyBinaryFile(filePath) {
|
|
258
|
+
return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
259
|
+
}
|
|
260
|
+
function adjustReplacementStringsForLineEndings(view, useCRLF) {
|
|
261
|
+
const out = { ...view };
|
|
262
|
+
for (const [key, value] of Object.entries(out)) {
|
|
263
|
+
if (typeof value === "string") {
|
|
264
|
+
out[key] = useCRLF ? value.replaceAll(/(?<!\r)\n/g, "\r\n") : value.replaceAll(/\r\n/g, "\n");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
async function collectWindowsFilesAsync(windowsRoot) {
|
|
270
|
+
const all = await collectWindowsPathsAsync(windowsRoot);
|
|
271
|
+
const files = new Array();
|
|
272
|
+
for (const p of all) {
|
|
273
|
+
try {
|
|
274
|
+
if ((await fs.lstat(p)).isFile()) {
|
|
275
|
+
files.push(p);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
// skip
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return files;
|
|
286
|
+
}
|
|
287
|
+
async function renderMustacheUnderWindowsAsync(windowsRoot, view) {
|
|
288
|
+
const filePaths = await collectWindowsFilesAsync(windowsRoot);
|
|
289
|
+
await Promise.all(filePaths.map(async (absoluteFilePath) => {
|
|
290
|
+
if (isProbablyBinaryFile(absoluteFilePath)) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
let content;
|
|
294
|
+
try {
|
|
295
|
+
content = await fs.readFile(absoluteFilePath, "utf8");
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!content.includes("{{")) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const useCRLF = content.includes("\r\n");
|
|
307
|
+
const adjustedView = adjustReplacementStringsForLineEndings(view, useCRLF);
|
|
308
|
+
const rendered = mustache.render(content, adjustedView);
|
|
309
|
+
if (rendered !== content) {
|
|
310
|
+
await fs.writeFile(absoluteFilePath, rendered, "utf8");
|
|
311
|
+
}
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
async function finalizeWindowsCppTemplateArtifacts(projectRoot, windowsRoot) {
|
|
315
|
+
const gitignoreRelPaths = await glob("**/_gitignore", {
|
|
316
|
+
cwd: windowsRoot,
|
|
317
|
+
nodir: true,
|
|
318
|
+
dot: true,
|
|
319
|
+
});
|
|
320
|
+
for (const rel of gitignoreRelPaths.sort().reverse()) {
|
|
321
|
+
const fromAbs = path.join(windowsRoot, rel);
|
|
322
|
+
const toAbs = path.join(windowsRoot, path.dirname(rel), ".gitignore");
|
|
323
|
+
await renameIfExistsPreferDest(fromAbs, toAbs);
|
|
324
|
+
}
|
|
325
|
+
await renameIfExistsPreferDest(path.join(projectRoot, "NuGet_Config"), path.join(projectRoot, "NuGet.config"));
|
|
326
|
+
const version = packageJson.version;
|
|
327
|
+
const banner = `<!-- This project was created with expo-desktop ${version} -->`;
|
|
328
|
+
const reactNativeWindowsBanner = /<!--\s*This project was created with react-native-windows[^\n\r]*-->/g;
|
|
329
|
+
const vcxprojRelPaths = await glob("windows/**/*.vcxproj", {
|
|
330
|
+
cwd: projectRoot,
|
|
331
|
+
nodir: true,
|
|
332
|
+
dot: true,
|
|
333
|
+
});
|
|
334
|
+
await Promise.all(vcxprojRelPaths.map(async (relPath) => {
|
|
335
|
+
const absolutePath = path.join(projectRoot, relPath);
|
|
336
|
+
let contents;
|
|
337
|
+
try {
|
|
338
|
+
contents = await fs.readFile(absolutePath, "utf8");
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const replaced = contents.replace(reactNativeWindowsBanner, banner);
|
|
344
|
+
if (replaced !== contents) {
|
|
345
|
+
await fs.writeFile(absolutePath, replaced, "utf8");
|
|
346
|
+
}
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
async function renameIfExistsPreferDest(from, to) {
|
|
350
|
+
try {
|
|
351
|
+
await fs.access(from);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await fs.rename(from, to);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") {
|
|
364
|
+
await fs.unlink(from);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { MacOSConfig } from "expo-desktop-config-plugins";
|
|
2
1
|
import { glob } from "glob";
|
|
3
2
|
import fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { MacOSConfig } = require("expo-desktop-config-plugins");
|
|
5
7
|
/**
|
|
6
8
|
* Returns a list of files within a template matched by the resolved rename
|
|
7
9
|
* config.
|
package/build/common/template.js
CHANGED
|
@@ -11,9 +11,10 @@ import fs from "node:fs/promises";
|
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { applyWindowsCppAppTemplateAsync } from "./apply-windows-cpp-app-template.js";
|
|
14
15
|
import { promisifiedSpawnTask } from "./child-process.js";
|
|
15
16
|
import { getTemplateFilesToRenameAsync, renameTemplateAppNameAsync, } from "./rename-template-app-name.js";
|
|
16
|
-
export async function applySelectedTemplatesAsync({ projectRoot, selection, enabledPlatforms, name, }) {
|
|
17
|
+
export async function applySelectedTemplatesAsync({ projectRoot, selection, enabledPlatforms, name, respectTemplateConfig, }) {
|
|
17
18
|
const descriptors = getOrderedTemplateDescriptors(selection, enabledPlatforms);
|
|
18
19
|
if (!descriptors.length) {
|
|
19
20
|
return;
|
|
@@ -29,17 +30,20 @@ export async function applySelectedTemplatesAsync({ projectRoot, selection, enab
|
|
|
29
30
|
// windows:
|
|
30
31
|
// - https://github.com/microsoft/react-native-windows/blob/3d64f71ed8495da6a0dcfc1f97bcb8f761986594/packages/%40react-native-windows/cli/src/generator-windows/index.ts#L57
|
|
31
32
|
// - https://github.com/microsoft/react-native-windows/tree/main/vnext/templates/cpp-app
|
|
32
|
-
for (const descriptor of descriptors) {
|
|
33
|
+
for (const [index, descriptor] of Object.entries(descriptors)) {
|
|
33
34
|
const source = parseTemplateSource(descriptor.value);
|
|
34
|
-
const extracted = await prepareTemplateSourceAsync(source);
|
|
35
|
+
const extracted = await prepareTemplateSourceAsync(`Extracting template ${parseInt(index) + 1}/${descriptors.length} (--template ${descriptor.key})`, source);
|
|
35
36
|
try {
|
|
36
37
|
const templateRoot = await resolveTemplateRootAsync(extracted, source);
|
|
37
|
-
const templateConfig =
|
|
38
|
+
const templateConfig = respectTemplateConfig
|
|
39
|
+
? await loadTemplateConfigAsync(templateRoot)
|
|
40
|
+
: undefined;
|
|
38
41
|
await copyTemplateFilesAsync({
|
|
39
42
|
sourceRoot: templateRoot,
|
|
40
43
|
projectRoot,
|
|
41
44
|
name,
|
|
42
45
|
templateConfig,
|
|
46
|
+
forPlatform: descriptor.forPlatform,
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
49
|
finally {
|
|
@@ -107,7 +111,7 @@ function parseTemplateSource(template) {
|
|
|
107
111
|
}
|
|
108
112
|
return { type: "npm", spec: template };
|
|
109
113
|
}
|
|
110
|
-
async function prepareTemplateSourceAsync(source) {
|
|
114
|
+
async function prepareTemplateSourceAsync(taskTitle, source) {
|
|
111
115
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "expo-desktop-template-"));
|
|
112
116
|
const archivePath = path.join(tempRoot, "template.tgz");
|
|
113
117
|
switch (source.type) {
|
|
@@ -144,7 +148,7 @@ async function prepareTemplateSourceAsync(source) {
|
|
|
144
148
|
}
|
|
145
149
|
await tasks([
|
|
146
150
|
promisifiedSpawnTask({
|
|
147
|
-
title:
|
|
151
|
+
title: taskTitle,
|
|
148
152
|
command: "tar",
|
|
149
153
|
args: ["-xzf", archivePath, "-C", tempRoot],
|
|
150
154
|
}),
|
|
@@ -172,12 +176,12 @@ async function loadTemplateConfigAsync(templateRoot) {
|
|
|
172
176
|
await fs.access(configPath);
|
|
173
177
|
}
|
|
174
178
|
catch {
|
|
175
|
-
return
|
|
179
|
+
return;
|
|
176
180
|
}
|
|
177
181
|
const imported = (await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href)));
|
|
178
182
|
return imported.default ?? imported;
|
|
179
183
|
}
|
|
180
|
-
async function copyTemplateFilesAsync({ sourceRoot, projectRoot, name, templateConfig, }) {
|
|
184
|
+
async function copyTemplateFilesAsync({ sourceRoot, projectRoot, name, templateConfig, forPlatform, }) {
|
|
181
185
|
const mappings = templateConfig?.files?.length
|
|
182
186
|
? templateConfig.files.map((mapping) => ({
|
|
183
187
|
from: path.join(sourceRoot, mapping.from),
|
|
@@ -213,6 +217,12 @@ async function copyTemplateFilesAsync({ sourceRoot, projectRoot, name, templateC
|
|
|
213
217
|
filesafeName: name.filesafeName,
|
|
214
218
|
files: filesToRename,
|
|
215
219
|
});
|
|
220
|
+
if (forPlatform === "macos") {
|
|
221
|
+
await renameMacosUnderscoreGitignore(projectRoot);
|
|
222
|
+
}
|
|
223
|
+
if (forPlatform === "windows") {
|
|
224
|
+
await applyWindowsCppAppTemplateAsync(projectRoot, name);
|
|
225
|
+
}
|
|
216
226
|
}
|
|
217
227
|
async function discoverAllFilesAsync(sourceRoot) {
|
|
218
228
|
const out = new Array();
|
|
@@ -263,3 +273,26 @@ async function applyExtraReplacementsAsync({ cwd, files, replacements, }) {
|
|
|
263
273
|
function normalizeToPosixPath(input) {
|
|
264
274
|
return input.replaceAll(path.sep, "/");
|
|
265
275
|
}
|
|
276
|
+
async function renameMacosUnderscoreGitignore(projectRoot) {
|
|
277
|
+
const from = path.join(projectRoot, "macos", "_gitignore");
|
|
278
|
+
const to = path.join(projectRoot, "macos", ".gitignore");
|
|
279
|
+
try {
|
|
280
|
+
await fs.access(from);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await fs.rename(from, to);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") {
|
|
293
|
+
await fs.unlink(from);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -12,9 +12,9 @@ export async function newExpoDesktopProject(args) {
|
|
|
12
12
|
if (skip) {
|
|
13
13
|
await createExpoDesktopApp({
|
|
14
14
|
name: {
|
|
15
|
-
displayName: "
|
|
16
|
-
filesafeName: "
|
|
17
|
-
rdns: "
|
|
15
|
+
displayName: "Your App Display Name",
|
|
16
|
+
filesafeName: "YourApp456",
|
|
17
|
+
rdns: "uk.co.birchlabs.your-app-456",
|
|
18
18
|
},
|
|
19
19
|
packageManager: "bun",
|
|
20
20
|
templates: {
|
|
@@ -42,6 +42,12 @@ export async function createExpoDesktopApp({ name, packageManager, templates, ve
|
|
|
42
42
|
selection: templateSelection,
|
|
43
43
|
enabledPlatforms: ["ios", "android", "macos", "windows"],
|
|
44
44
|
name,
|
|
45
|
+
// I had originally hoped to consume the template.config.js file provided by
|
|
46
|
+
// the template, but the prototype in cpp-app imports dependencies like
|
|
47
|
+
// "chalk", "lodash", "username", and "../templateUtils" that it doesn't
|
|
48
|
+
// declare in any package.json, so we can't reliably support it. Will
|
|
49
|
+
// revisit the idea in future.
|
|
50
|
+
respectTemplateConfig: false,
|
|
45
51
|
});
|
|
46
52
|
console.log(`${green("◆")} Applied templates.\n`);
|
|
47
53
|
title("Altering app.json…", { spacing: 1 });
|
|
@@ -52,6 +52,7 @@ export async function prebuild({ clean, "no-install": noInstall, npm, yarn, bun,
|
|
|
52
52
|
selection: templateSelection,
|
|
53
53
|
enabledPlatforms: platforms,
|
|
54
54
|
name: appName,
|
|
55
|
+
respectTemplateConfig: false,
|
|
55
56
|
});
|
|
56
57
|
log.info("Applied project templates for clean prebuild.", { withGuide: false });
|
|
57
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-desktop",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Best-effort desktop support for Expo",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|
|
@@ -39,14 +39,16 @@
|
|
|
39
39
|
"citty": "^0.2.2",
|
|
40
40
|
"glob": "^10.5.0",
|
|
41
41
|
"kleur": "^4.1.5",
|
|
42
|
+
"mustache": "^4.2.0",
|
|
42
43
|
"toml": "^4.1.1",
|
|
43
|
-
"expo-desktop-config-plugins": "1.1.
|
|
44
|
-
"expo-desktop-prebuild-config": "1.0.
|
|
44
|
+
"expo-desktop-config-plugins": "1.1.18",
|
|
45
|
+
"expo-desktop-prebuild-config": "1.0.7"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@expo/config": "^12.0.13",
|
|
48
49
|
"@expo/config-plugins": "^54.0.4",
|
|
49
50
|
"@tsconfig/node24": "^24.0.4",
|
|
51
|
+
"@types/mustache": "^4.2.6",
|
|
50
52
|
"@types/node": "^24.12.2",
|
|
51
53
|
"@typescript/native-preview": "^7.0.0-dev.20260425.1",
|
|
52
54
|
"vitest": "^3.2.4"
|