expo-desktop 0.1.23 → 0.1.25
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.
|
@@ -30,7 +30,7 @@ export async function applyWindowsCppAppTemplateAsync(projectRoot, name) {
|
|
|
30
30
|
const replacements = await buildReplacementsRecord(projectRoot, name);
|
|
31
31
|
await renameCppAppPathsAsync(windowsRoot, name.filesafeName);
|
|
32
32
|
await renderMustacheUnderWindowsAsync(windowsRoot, replacements);
|
|
33
|
-
await finalizeWindowsCppTemplateArtifacts(projectRoot
|
|
33
|
+
await finalizeWindowsCppTemplateArtifacts(projectRoot);
|
|
34
34
|
}
|
|
35
35
|
async function looksLikeCppAppTemplateAsync(windowsRoot) {
|
|
36
36
|
try {
|
|
@@ -128,19 +128,33 @@ async function buildReplacementsRecord(projectRoot, name) {
|
|
|
128
128
|
autolinkCppPackageProviders: "\n UNREFERENCED_PARAMETER(packageProviders);",
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
/** Placeholder app name baked into the react-native-windows cpp-app template. */
|
|
132
|
+
const TEMPLATE_APP_NAME_PLACEHOLDER = "MyApp";
|
|
133
|
+
/**
|
|
134
|
+
* Basenames in the cpp-app template that need a one-shot literal rename
|
|
135
|
+
* (independent of any `MyApp` substitution).
|
|
136
|
+
*/
|
|
137
|
+
const SPECIAL_BASENAME_RENAMES = {
|
|
138
|
+
_gitignore: ".gitignore",
|
|
139
|
+
NuGet_Config: "NuGet.config",
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Returns the renamed basename for one entry in the cpp-app template, applying
|
|
143
|
+
* (in order):
|
|
144
|
+
*
|
|
145
|
+
* 1. {@link SPECIAL_BASENAME_RENAMES} (e.g. `_gitignore` → `.gitignore`).
|
|
146
|
+
* 2. `MyApp` → {@link filesafeName}, anywhere in the basename.
|
|
147
|
+
*
|
|
148
|
+
* If the user's `filesafeName` happens to equal `"MyApp"`, step (2) is a no-op
|
|
149
|
+
* (avoids degenerate self-renames that previously hung
|
|
150
|
+
* `renameCppAppPathsAsync`).
|
|
151
|
+
*/
|
|
152
|
+
function renameTemplateBasename(basename, filesafeName) {
|
|
153
|
+
const renamed = SPECIAL_BASENAME_RENAMES[basename] ?? basename;
|
|
154
|
+
if (filesafeName === TEMPLATE_APP_NAME_PLACEHOLDER) {
|
|
155
|
+
return renamed;
|
|
139
156
|
}
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
function pathDepth(p) {
|
|
143
|
-
return p.split(path.sep).filter(Boolean).length;
|
|
157
|
+
return renamed.split(TEMPLATE_APP_NAME_PLACEHOLDER).join(filesafeName);
|
|
144
158
|
}
|
|
145
159
|
async function collectWindowsPathsAsync(windowsRoot) {
|
|
146
160
|
const out = [];
|
|
@@ -173,72 +187,46 @@ async function collectWindowsPathsAsync(windowsRoot) {
|
|
|
173
187
|
return out;
|
|
174
188
|
}
|
|
175
189
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
190
|
+
* Walks `windowsRoot` pre-order (parents before children), renaming each entry
|
|
191
|
+
* by its basename via {@link renameTemplateBasename}:
|
|
192
|
+
*
|
|
193
|
+
* - `MyApp` (anywhere in the basename) → `filesafeName`
|
|
194
|
+
* - `_gitignore` → `.gitignore`
|
|
195
|
+
* - `NuGet_Config` → `NuGet.config`
|
|
196
|
+
*
|
|
197
|
+
* Pre-order is essential. `fs.rename()` atomically moves an entire directory
|
|
198
|
+
* subtree, so renaming a parent like `MyApp.Package` → `YourApp.Package`
|
|
199
|
+
* already relocates everything beneath it. A bottom-up order would risk
|
|
200
|
+
* `ENOTEMPTY` when the parent later tries to overwrite a non-empty
|
|
201
|
+
* destination.
|
|
202
|
+
*
|
|
203
|
+
* Symlinks are skipped to avoid escaping the template tree. Empty directories
|
|
204
|
+
* are handled naturally — `readdir` simply returns no children.
|
|
180
205
|
*/
|
|
181
206
|
async function renameCppAppPathsAsync(windowsRoot, filesafeName) {
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const rel = path.relative(windowsRoot, abs);
|
|
187
|
-
if (!rel || rel.includes("..")) {
|
|
207
|
+
async function walk(dir) {
|
|
208
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
if (entry.isSymbolicLink()) {
|
|
188
211
|
continue;
|
|
189
212
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
throw error;
|
|
213
|
+
const sourcePath = path.join(dir, entry.name);
|
|
214
|
+
const newBasename = renameTemplateBasename(entry.name, filesafeName);
|
|
215
|
+
const targetPath = newBasename === entry.name ? sourcePath : path.join(dir, newBasename);
|
|
216
|
+
if (targetPath !== sourcePath) {
|
|
217
|
+
try {
|
|
218
|
+
await fs.rename(sourcePath, targetPath);
|
|
219
|
+
}
|
|
220
|
+
catch (cause) {
|
|
221
|
+
throw new Error(`Failed to rename "${sourcePath}" → "${targetPath}"`, { cause });
|
|
200
222
|
}
|
|
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
223
|
}
|
|
220
|
-
if (
|
|
221
|
-
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
await walk(targetPath);
|
|
222
226
|
}
|
|
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
227
|
}
|
|
241
228
|
}
|
|
229
|
+
await walk(windowsRoot);
|
|
242
230
|
}
|
|
243
231
|
const BINARY_EXTENSIONS = new Set([
|
|
244
232
|
".png",
|
|
@@ -311,17 +299,7 @@ async function renderMustacheUnderWindowsAsync(windowsRoot, view) {
|
|
|
311
299
|
}
|
|
312
300
|
}));
|
|
313
301
|
}
|
|
314
|
-
async function finalizeWindowsCppTemplateArtifacts(projectRoot
|
|
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
|
-
}
|
|
302
|
+
async function finalizeWindowsCppTemplateArtifacts(projectRoot) {
|
|
325
303
|
await renameIfExistsPreferDest(path.join(projectRoot, "NuGet_Config"), path.join(projectRoot, "NuGet.config"));
|
|
326
304
|
const version = packageJson.version;
|
|
327
305
|
const banner = `<!-- This project was created with expo-desktop ${version} -->`;
|
|
@@ -39,7 +39,7 @@ function runPromisifiedSpawn({ command, args, options, logLine, debugLogDir, })
|
|
|
39
39
|
stdio: stdioEffective,
|
|
40
40
|
env: envWithForcedColorIfPiped({ ...options, stdio: stdioEffective }),
|
|
41
41
|
};
|
|
42
|
-
const cp = spawn(command
|
|
42
|
+
const cp = spawn(`${command} ${args.join(" ")}`, spawnOptions);
|
|
43
43
|
/** Interleaved stdout/stderr lines in arrival order (tagged for readability). */
|
|
44
44
|
const lineBuffer = [];
|
|
45
45
|
const pushLine = (stream, line) => {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Shescape } from "shescape";
|
|
2
|
+
const defaultShell = Symbol("Default shell");
|
|
3
|
+
const shescapes = {};
|
|
4
|
+
export function getShescape(shell) {
|
|
5
|
+
const resolvedShell = shell ?? defaultShell;
|
|
6
|
+
if (shescapes[resolvedShell]) {
|
|
7
|
+
return shescapes[resolvedShell];
|
|
8
|
+
}
|
|
9
|
+
const shescapeOptions = {};
|
|
10
|
+
if (typeof resolvedShell === "string") {
|
|
11
|
+
shescapeOptions.shell = resolvedShell;
|
|
12
|
+
}
|
|
13
|
+
let shescape;
|
|
14
|
+
try {
|
|
15
|
+
shescape = new Shescape(shescapeOptions);
|
|
16
|
+
}
|
|
17
|
+
catch (cause) {
|
|
18
|
+
if (!(cause instanceof Error) || cause.message !== "Shescape does not support the shell sh") {
|
|
19
|
+
throw new Error("Unable to spawn child process due to error being thrown when constructing Shescape instance", { cause });
|
|
20
|
+
}
|
|
21
|
+
// Can't escape for the meta-shell `/bin/sh`. Let's try falling back to a
|
|
22
|
+
// typical Unix shell and hoping for the best.
|
|
23
|
+
// https://github.com/ericcornelissen/shescape/issues/2009
|
|
24
|
+
try {
|
|
25
|
+
shescapeOptions.shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
26
|
+
shescape = new Shescape(shescapeOptions);
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
throw new Error("Unable to spawn child process due to error being thrown when constructing fallback Shescape instance", { cause });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
shescapes[resolvedShell] = shescape;
|
|
33
|
+
return shescape;
|
|
34
|
+
}
|
package/build/common/template.js
CHANGED
|
@@ -14,6 +14,7 @@ import { pathToFileURL } from "node:url";
|
|
|
14
14
|
import { applyWindowsCppAppTemplateAsync } from "./apply-windows-cpp-app-template.js";
|
|
15
15
|
import { promisifiedSpawnTask } from "./child-process.js";
|
|
16
16
|
import { getTemplateFilesToRenameAsync, renameTemplateAppNameAsync, } from "./rename-template-app-name.js";
|
|
17
|
+
import { getShescape } from "./shescape.js";
|
|
17
18
|
export async function applySelectedTemplatesAsync({ projectRoot, selection, enabledPlatforms, name, respectTemplateConfig, }) {
|
|
18
19
|
const descriptors = getOrderedTemplateDescriptors(selection, enabledPlatforms);
|
|
19
20
|
if (!descriptors.length) {
|
|
@@ -112,6 +113,8 @@ function parseTemplateSource(template) {
|
|
|
112
113
|
return { type: "npm", spec: template };
|
|
113
114
|
}
|
|
114
115
|
async function prepareTemplateSourceAsync(taskTitle, source) {
|
|
116
|
+
// We make sure there are no spaces in the path so that we don't need to
|
|
117
|
+
// quote/escape the shell command.
|
|
115
118
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "expo-desktop-template-"));
|
|
116
119
|
const archivePath = path.join(tempRoot, "template.tgz");
|
|
117
120
|
switch (source.type) {
|
|
@@ -119,6 +122,8 @@ async function prepareTemplateSourceAsync(taskTitle, source) {
|
|
|
119
122
|
await fs.copyFile(source.path, archivePath);
|
|
120
123
|
break;
|
|
121
124
|
case "github": {
|
|
125
|
+
// Don't think it's possible to have spaces in the owner/repo/ref, so no
|
|
126
|
+
// percent-encoding or quoting needed.
|
|
122
127
|
const tarballUrl = `https://codeload.github.com/${source.owner}/${source.repo}/tar.gz/${source.ref}`;
|
|
123
128
|
const response = await fetch(tarballUrl);
|
|
124
129
|
if (!response.ok || !response.body) {
|
|
@@ -129,11 +134,12 @@ async function prepareTemplateSourceAsync(taskTitle, source) {
|
|
|
129
134
|
break;
|
|
130
135
|
}
|
|
131
136
|
case "npm": {
|
|
137
|
+
const shescape = getShescape();
|
|
132
138
|
await tasks([
|
|
133
139
|
promisifiedSpawnTask({
|
|
134
140
|
title: `npm pack (${source.spec})`,
|
|
135
141
|
command: "npm",
|
|
136
|
-
args: ["pack", source.spec, "--silent"],
|
|
142
|
+
args: ["pack", shescape.quote(source.spec), "--silent"],
|
|
137
143
|
options: { cwd: tempRoot },
|
|
138
144
|
}),
|
|
139
145
|
]);
|
|
@@ -149,7 +155,11 @@ async function prepareTemplateSourceAsync(taskTitle, source) {
|
|
|
149
155
|
await tasks([
|
|
150
156
|
promisifiedSpawnTask({
|
|
151
157
|
title: taskTitle,
|
|
152
|
-
|
|
158
|
+
// The paths are Windows-style paths rather than POSIX, so make sure to
|
|
159
|
+
// select Windows tar rather than GNU tar (which may be on path, even when
|
|
160
|
+
// invoking from cmd.exe, due to having git bash installed).
|
|
161
|
+
// tar C:\Users\Jamie\AppData\Local\Temp\expo-desktop-template-Uf1F1B\template.tgz -C C:\Users\Jamie\AppData\Local\Temp\expo-desktop-template-Uf1F1B
|
|
162
|
+
command: process.platform === "win32" ? "C:\\Windows\\System32\\tar.exe" : "tar",
|
|
153
163
|
args: ["-xzf", archivePath, "-C", tempRoot],
|
|
154
164
|
}),
|
|
155
165
|
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-desktop",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"description": "Best-effort desktop support for Expo",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"glob": "^10.5.0",
|
|
43
43
|
"kleur": "^4.1.5",
|
|
44
44
|
"mustache": "^4.2.0",
|
|
45
|
+
"shescape": "^2.1.12",
|
|
45
46
|
"toml": "^4.1.1"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|