@limrun/api 0.39.3 → 0.39.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/CHANGELOG.md +8 -0
- package/folder-sync-ignore.d.mts +38 -0
- package/folder-sync-ignore.d.mts.map +1 -1
- package/folder-sync-ignore.d.ts +38 -0
- package/folder-sync-ignore.d.ts.map +1 -1
- package/folder-sync-ignore.js +107 -12
- package/folder-sync-ignore.js.map +1 -1
- package/folder-sync-ignore.mjs +107 -12
- package/folder-sync-ignore.mjs.map +1 -1
- package/folder-sync.d.mts +6 -0
- package/folder-sync.d.mts.map +1 -1
- package/folder-sync.d.ts +6 -0
- package/folder-sync.d.ts.map +1 -1
- package/folder-sync.js +153 -13
- package/folder-sync.js.map +1 -1
- package/folder-sync.mjs +153 -13
- package/folder-sync.mjs.map +1 -1
- package/ios-client.d.mts +5 -5
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +5 -5
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +18 -22
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +18 -22
- package/ios-client.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/xcode-instances-helpers.d.mts +6 -0
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +6 -0
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +7 -25
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +7 -25
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/folder-sync-ignore.ts +128 -12
- package/src/folder-sync.ts +189 -17
- package/src/ios-client.ts +24 -28
- package/src/resources/xcode-instances-helpers.ts +13 -29
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/folder-sync.ts
CHANGED
|
@@ -47,6 +47,12 @@ export type FolderSyncOptions = {
|
|
|
47
47
|
* ignoreFn: (path) => !(path.startsWith('src/') || path.endsWith('.json'))
|
|
48
48
|
*/
|
|
49
49
|
ignoreFn: IgnoreFn;
|
|
50
|
+
/**
|
|
51
|
+
* Sync symlinks as symlinks (relative, in-root targets only). Off by
|
|
52
|
+
* default: only the limbuild workspace sync understands link entries;
|
|
53
|
+
* the limulator app-install sync keeps the historical skip behavior.
|
|
54
|
+
*/
|
|
55
|
+
syncSymlinks?: boolean;
|
|
50
56
|
/**
|
|
51
57
|
* Extra files to sync into limbuild. These are included
|
|
52
58
|
* in every sync pass but are not watched directly.
|
|
@@ -77,6 +83,12 @@ type FileEntry = {
|
|
|
77
83
|
sha256: string;
|
|
78
84
|
absPath: string;
|
|
79
85
|
mode: number;
|
|
86
|
+
/**
|
|
87
|
+
* Symlink target (literal readlink output, forward slashes). Non-empty
|
|
88
|
+
* marks a symlink entry: the target string is the content (sha256/size
|
|
89
|
+
* describe it) and the entry never carries a payload.
|
|
90
|
+
*/
|
|
91
|
+
linkTarget?: string;
|
|
80
92
|
};
|
|
81
93
|
|
|
82
94
|
type FolderSyncHttpPayload = {
|
|
@@ -94,7 +106,7 @@ type FolderSyncHttpMeta = {
|
|
|
94
106
|
rootName: string;
|
|
95
107
|
install?: boolean;
|
|
96
108
|
launchMode?: 'ForegroundIfRunning' | 'RelaunchIfRunning';
|
|
97
|
-
files: { path: string; size: number; sha256: string; mode
|
|
109
|
+
files: { path: string; size: number; sha256: string; mode?: number; link?: string }[];
|
|
98
110
|
payloads: FolderSyncHttpPayload[];
|
|
99
111
|
};
|
|
100
112
|
type FolderSyncHttpResponse = {
|
|
@@ -109,6 +121,10 @@ type FolderSyncHttpResponse = {
|
|
|
109
121
|
error?: string;
|
|
110
122
|
};
|
|
111
123
|
|
|
124
|
+
/** Full uploads at or above this size get an info-level notice so "sync is
|
|
125
|
+
* stuck" turns into "a GB-scale file is being uploaded". */
|
|
126
|
+
const LARGE_UPLOAD_NOTICE_BYTES = 64 * 1024 * 1024;
|
|
127
|
+
|
|
112
128
|
const noopLogger = (_level: 'debug' | 'info' | 'warn' | 'error', _msg: string) => {
|
|
113
129
|
// Intentionally empty: callers (e.g. ios-client.ts) should provide their own logger
|
|
114
130
|
// to control verbosity and integrate with the SDK's logging setup.
|
|
@@ -269,7 +285,35 @@ async function sha256FileHex(filePath: string): Promise<string> {
|
|
|
269
285
|
});
|
|
270
286
|
}
|
|
271
287
|
|
|
272
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Validates a symlink's readlink target at sync time: it must be relative,
|
|
290
|
+
* backslash-free, and lexically resolve inside the sync root when joined
|
|
291
|
+
* with the link's directory (the daemon enforces the same policy on apply,
|
|
292
|
+
* so failing here is the same failure with a better message).
|
|
293
|
+
*/
|
|
294
|
+
function validateSymlinkTarget(rel: string, target: string): void {
|
|
295
|
+
if (target.includes('\\')) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`symlink ${rel} -> ${target} target contains a backslash, which the daemon rejects; ` +
|
|
298
|
+
'remove the link or pass --ignore to skip it',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(rel), target));
|
|
302
|
+
if (resolved === '.' || resolved === '..' || resolved.startsWith('../')) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`symlink ${rel} -> ${target} points outside the sync root; ` +
|
|
305
|
+
'run the sync from the repo root that contains the target, remove the link, ' +
|
|
306
|
+
'or pass --ignore to skip it',
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function walkFiles(
|
|
312
|
+
root: string,
|
|
313
|
+
ignoreFn: IgnoreFn,
|
|
314
|
+
syncSymlinks: boolean,
|
|
315
|
+
log?: FolderSyncOptions['log'],
|
|
316
|
+
): Promise<FileEntry[]> {
|
|
273
317
|
const rootResolved = path.resolve(root);
|
|
274
318
|
const rootStat = await fs.promises.stat(rootResolved);
|
|
275
319
|
if (rootStat.isFile()) {
|
|
@@ -305,6 +349,36 @@ async function walkFiles(root: string, ignoreFn: IgnoreFn): Promise<FileEntry[]>
|
|
|
305
349
|
stack.push(abs);
|
|
306
350
|
continue;
|
|
307
351
|
}
|
|
352
|
+
if (ent.isSymbolicLink()) {
|
|
353
|
+
if (!syncSymlinks) continue;
|
|
354
|
+
// Symlinks ship as link entries with the literal target: a
|
|
355
|
+
// symlinked directory arrives here too and is sent as a link, not
|
|
356
|
+
// traversed (git semantics). The target string is the content.
|
|
357
|
+
// Probe both forms: a symlink named e.g. `Pods` or `build` may point
|
|
358
|
+
// at a directory, so directory-only excludes (`Pods/`, `build/`)
|
|
359
|
+
// must match it and skip before validation rejects an out-of-root
|
|
360
|
+
// target.
|
|
361
|
+
if (ignoreFn(rel) || ignoreFn(rel + '/')) continue;
|
|
362
|
+
const target = (await fs.promises.readlink(abs)).split(path.sep).join('/');
|
|
363
|
+
// An absolute target can never resolve remotely and no sync root
|
|
364
|
+
// can contain it; skip it like pre-symlink clients did (with a
|
|
365
|
+
// warning) instead of failing a previously working sync.
|
|
366
|
+
if (path.posix.isAbsolute(target) || path.win32.isAbsolute(target)) {
|
|
367
|
+
log?.('warn', `skipping symlink ${rel} -> ${target}: absolute targets cannot resolve remotely`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
validateSymlinkTarget(rel, target);
|
|
371
|
+
const sha256 = crypto.createHash('sha256').update(target).digest('hex');
|
|
372
|
+
out.push({
|
|
373
|
+
path: rel,
|
|
374
|
+
size: Buffer.byteLength(target),
|
|
375
|
+
sha256,
|
|
376
|
+
absPath: abs,
|
|
377
|
+
mode: 0,
|
|
378
|
+
linkTarget: target,
|
|
379
|
+
});
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
308
382
|
if (!ent.isFile()) continue;
|
|
309
383
|
|
|
310
384
|
// Check custom ignores for files
|
|
@@ -329,7 +403,9 @@ async function collectAdditionalFiles(
|
|
|
329
403
|
}
|
|
330
404
|
const out: FileEntry[] = [];
|
|
331
405
|
for (const additionalFile of additionalFiles) {
|
|
332
|
-
|
|
406
|
+
// Canonicalize user-supplied remote paths ("./netrc" -> "netrc") so the
|
|
407
|
+
// manifest key matches what the daemon applies and echoes in needFull.
|
|
408
|
+
const remotePath = path.posix.normalize(additionalFile.remotePath);
|
|
333
409
|
const absPath = path.resolve(additionalFile.localPath);
|
|
334
410
|
const st = await fs.promises.stat(absPath);
|
|
335
411
|
if (!st.isFile()) {
|
|
@@ -449,9 +525,48 @@ export async function encodeXdelta3Patch(
|
|
|
449
525
|
return bytesWritten;
|
|
450
526
|
}
|
|
451
527
|
|
|
452
|
-
async function cachePut(
|
|
453
|
-
|
|
528
|
+
async function cachePut(
|
|
529
|
+
cacheRoot: string,
|
|
530
|
+
relPath: string,
|
|
531
|
+
srcFile: string,
|
|
532
|
+
linkTarget?: string,
|
|
533
|
+
checkedDirs?: Set<string>,
|
|
534
|
+
): Promise<void> {
|
|
535
|
+
const segments = relPath.split('/');
|
|
536
|
+
const dst = path.join(cacheRoot, segments.join(path.sep));
|
|
537
|
+
// Remove any ancestor that is not a real directory before mkdir/write: a
|
|
538
|
+
// path that previously synced as a symlink (or a regular file that became
|
|
539
|
+
// a directory) leaves a stale cache inode, and writing children through a
|
|
540
|
+
// symlink would corrupt the link target's cached entries while mkdir over
|
|
541
|
+
// a file throws after the server already applied. Only relevant when
|
|
542
|
+
// symlink sync is on; the caller passes checkedDirs (which also memoizes
|
|
543
|
+
// ancestors already confirmed real, so the sorted walk skips shared
|
|
544
|
+
// prefixes) then and omits it otherwise to skip the sweep entirely.
|
|
545
|
+
if (checkedDirs) {
|
|
546
|
+
let ancestor = cacheRoot;
|
|
547
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
548
|
+
ancestor = path.join(ancestor, segments[i]!);
|
|
549
|
+
if (checkedDirs.has(ancestor)) continue;
|
|
550
|
+
const ast = await fs.promises.lstat(ancestor).catch(() => null);
|
|
551
|
+
if (ast && !ast.isDirectory()) {
|
|
552
|
+
await fs.promises.rm(ancestor, { recursive: true, force: true });
|
|
553
|
+
} else if (ast) {
|
|
554
|
+
checkedDirs.add(ancestor);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
454
558
|
await fs.promises.mkdir(path.dirname(dst), { recursive: true });
|
|
559
|
+
// Replace the destination inode when its type doesn't match what we're
|
|
560
|
+
// writing: a stale symlink (copyFile would follow it) or a stale directory
|
|
561
|
+
// (copyFile onto a dir throws EISDIR) left by a since-changed path.
|
|
562
|
+
const st = await fs.promises.lstat(dst).catch(() => null);
|
|
563
|
+
if (st && (st.isSymbolicLink() || st.isDirectory() || linkTarget !== undefined)) {
|
|
564
|
+
await fs.promises.rm(dst, { recursive: true, force: true });
|
|
565
|
+
}
|
|
566
|
+
if (linkTarget !== undefined) {
|
|
567
|
+
await fs.promises.symlink(linkTarget, dst);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
455
570
|
await fs.promises.copyFile(srcFile, dst);
|
|
456
571
|
}
|
|
457
572
|
|
|
@@ -567,7 +682,7 @@ async function syncFolderOnce(
|
|
|
567
682
|
const log = opts.log ?? noopLogger;
|
|
568
683
|
const slog = (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => log(level, `syncFolder: ${msg}`);
|
|
569
684
|
|
|
570
|
-
const files = await walkFiles(localFolderPath, opts.ignoreFn);
|
|
685
|
+
const files = await walkFiles(localFolderPath, opts.ignoreFn, opts.syncSymlinks ?? false, slog);
|
|
571
686
|
const additionalFiles = await collectAdditionalFiles(opts.additionalFiles);
|
|
572
687
|
const allFiles = [...files, ...additionalFiles].sort((a, b) => a.path.localeCompare(b.path));
|
|
573
688
|
const fileMap = new Map(allFiles.map((f) => [f.path, f]));
|
|
@@ -587,11 +702,30 @@ async function syncFolderOnce(
|
|
|
587
702
|
type EncodedPayload = { payload: FolderSyncHttpPayload; filePath: string; cleanupDir?: string };
|
|
588
703
|
|
|
589
704
|
// Build payload list by comparing against local basis cache (single-flight/watch assumes server matches cache).
|
|
705
|
+
// lstat, never stat/existsSync: those follow symlinks and misreport a
|
|
706
|
+
// basis symlink (dangling or pointing at another cached file) as the
|
|
707
|
+
// file itself.
|
|
590
708
|
const encodeLimit = concurrencyLimit();
|
|
591
709
|
const changed: FileEntry[] = [];
|
|
592
710
|
for (const f of allFiles) {
|
|
593
711
|
const basisPath = cacheGet(opts.basisCacheDir, f.path);
|
|
594
|
-
|
|
712
|
+
const basisStat = await fs.promises.lstat(basisPath).catch(() => null);
|
|
713
|
+
if (!basisStat) {
|
|
714
|
+
changed.push(f);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (f.linkTarget !== undefined) {
|
|
718
|
+
if (!basisStat.isSymbolicLink()) {
|
|
719
|
+
changed.push(f);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const basisTarget = (await fs.promises.readlink(basisPath)).split(path.sep).join('/');
|
|
723
|
+
if (basisTarget !== f.linkTarget) {
|
|
724
|
+
changed.push(f);
|
|
725
|
+
}
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (!basisStat.isFile()) {
|
|
595
729
|
changed.push(f);
|
|
596
730
|
continue;
|
|
597
731
|
}
|
|
@@ -601,9 +735,17 @@ async function syncFolderOnce(
|
|
|
601
735
|
}
|
|
602
736
|
}
|
|
603
737
|
|
|
604
|
-
|
|
738
|
+
// Symlink entries travel in the manifest only, with no payloads.
|
|
739
|
+
const changedFiles = changed.filter((f) => f.linkTarget === undefined);
|
|
740
|
+
// Grows in the needFull retry below; sized here so the summary counts
|
|
741
|
+
// every path that actually shipped this sync.
|
|
742
|
+
const changedPaths = new Set(changed.map((f) => f.path));
|
|
743
|
+
|
|
744
|
+
const encodedPayloads = await mapLimit(changedFiles, encodeLimit, async (f): Promise<EncodedPayload> => {
|
|
605
745
|
const basisPath = cacheGet(opts.basisCacheDir, f.path);
|
|
606
|
-
|
|
746
|
+
const basisStat =
|
|
747
|
+
f.size >= MIN_XDELTA_TARGET_BYTES ? await fs.promises.lstat(basisPath).catch(() => null) : null;
|
|
748
|
+
if (basisStat?.isFile()) {
|
|
607
749
|
const basisSha = await sha256FileHex(basisPath);
|
|
608
750
|
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'limulator-xdelta3-'));
|
|
609
751
|
const patchPath = path.join(tmpDir, 'patch.xdelta3');
|
|
@@ -642,6 +784,9 @@ async function syncFolderOnce(
|
|
|
642
784
|
}
|
|
643
785
|
}
|
|
644
786
|
slog('debug', `full(file): ${f.path} size=${f.size}`);
|
|
787
|
+
if (f.size >= LARGE_UPLOAD_NOTICE_BYTES) {
|
|
788
|
+
slog('info', `uploading large file ${f.path} (${fmtBytes(f.size)})`);
|
|
789
|
+
}
|
|
645
790
|
bytesSentFull += f.size;
|
|
646
791
|
return {
|
|
647
792
|
payload: {
|
|
@@ -663,7 +808,8 @@ async function syncFolderOnce(
|
|
|
663
808
|
path: f.path,
|
|
664
809
|
size: f.size,
|
|
665
810
|
sha256: f.sha256.toLowerCase(),
|
|
666
|
-
mode
|
|
811
|
+
// Symlink entries carry the target; regular files carry the mode bits.
|
|
812
|
+
...(f.linkTarget !== undefined ? { link: f.linkTarget } : { mode: f.mode }),
|
|
667
813
|
})),
|
|
668
814
|
payloads: encodedPayloads.map((p) => p.payload),
|
|
669
815
|
};
|
|
@@ -696,10 +842,29 @@ async function syncFolderOnce(
|
|
|
696
842
|
// Retry once if server needs full for some paths (basis mismatch).
|
|
697
843
|
if (!resp.ok && resp.needFull && resp.needFull.length > 0) {
|
|
698
844
|
const need = new Set(resp.needFull);
|
|
845
|
+
// A daemon that predates symlink support json-drops the `link` field and
|
|
846
|
+
// asks for these entries as full files. Streaming absPath would FOLLOW
|
|
847
|
+
// the link and silently materialize target content remotely, so fail loud
|
|
848
|
+
// instead. (New daemons never put symlink entries in needFull.)
|
|
849
|
+
const needFullLinks = [...need].filter((p) => fileMap.get(p)?.linkTarget !== undefined);
|
|
850
|
+
if (needFullLinks.length > 0) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
"this instance's build daemon does not support symlinks yet (needs an updated limbuild); " +
|
|
853
|
+
`recreate the instance or retry later. Paths: ${needFullLinks.join(', ')}`,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
699
856
|
const retryPayloads: EncodedPayload[] = [];
|
|
700
857
|
for (const p of need) {
|
|
701
858
|
const entry = fileMap.get(p);
|
|
702
859
|
if (!entry) continue;
|
|
860
|
+
// The retry uploads real bytes: count them (and surface large files)
|
|
861
|
+
// exactly like first-pass fulls, or a fresh-daemon resync of a warm
|
|
862
|
+
// cache reports "sent=0B" while gigabytes upload.
|
|
863
|
+
if (entry.size >= LARGE_UPLOAD_NOTICE_BYTES) {
|
|
864
|
+
slog('info', `uploading large file ${entry.path} (${fmtBytes(entry.size)})`);
|
|
865
|
+
}
|
|
866
|
+
bytesSentFull += entry.size;
|
|
867
|
+
changedPaths.add(entry.path);
|
|
703
868
|
retryPayloads.push({
|
|
704
869
|
payload: {
|
|
705
870
|
kind: 'full',
|
|
@@ -709,10 +874,12 @@ async function syncFolderOnce(
|
|
|
709
874
|
},
|
|
710
875
|
filePath: entry.absPath,
|
|
711
876
|
});
|
|
712
|
-
bytesSentFull += entry.size;
|
|
713
877
|
}
|
|
714
878
|
if (retryPayloads.length > 0) {
|
|
715
|
-
slog(
|
|
879
|
+
slog(
|
|
880
|
+
'info',
|
|
881
|
+
`daemon requested a full upload for ${retryPayloads.length} files (no matching basis); retrying once`,
|
|
882
|
+
);
|
|
716
883
|
const retryMeta: FolderSyncHttpMeta = {
|
|
717
884
|
...meta,
|
|
718
885
|
id: genId('sync'),
|
|
@@ -749,11 +916,12 @@ async function syncFolderOnce(
|
|
|
749
916
|
const tookMs = nowMs() - totalStart;
|
|
750
917
|
const totalBytes = bytesSentFull + bytesSentDelta;
|
|
751
918
|
slog(
|
|
752
|
-
'
|
|
753
|
-
`sync
|
|
754
|
-
|
|
755
|
-
)}
|
|
919
|
+
'info',
|
|
920
|
+
`sync complete: files=${allFiles.length} changed=${changedPaths.size} sent=${fmtBytes(
|
|
921
|
+
totalBytes,
|
|
922
|
+
)} in ${fmtMs(tookMs)}`,
|
|
756
923
|
);
|
|
924
|
+
slog('debug', `sync timing syncWork=${fmtMs(syncWorkMs)} total=${fmtMs(tookMs)}`);
|
|
757
925
|
const out: SyncFolderResult = { bytesSent: totalBytes };
|
|
758
926
|
if (resp.installedAppPath) {
|
|
759
927
|
out.installedAppPath = resp.installedAppPath;
|
|
@@ -762,8 +930,12 @@ async function syncFolderOnce(
|
|
|
762
930
|
out.installedBundleId = resp.bundleId;
|
|
763
931
|
}
|
|
764
932
|
// Update local cache optimistically: after a successful sync, cache reflects current local tree.
|
|
933
|
+
// The stale-symlink-ancestor sweep only matters when symlink sync is on
|
|
934
|
+
// (otherwise the cache never holds a symlink); checkedDirs memoizes ancestors
|
|
935
|
+
// across the sorted walk.
|
|
936
|
+
const checkedDirs = opts.syncSymlinks ? new Set<string>() : undefined;
|
|
765
937
|
for (const f of allFiles) {
|
|
766
|
-
await cachePut(opts.basisCacheDir, f.path, f.absPath);
|
|
938
|
+
await cachePut(opts.basisCacheDir, f.path, f.absPath, f.linkTarget, checkedDirs);
|
|
767
939
|
}
|
|
768
940
|
return out;
|
|
769
941
|
}
|
package/src/ios-client.ts
CHANGED
|
@@ -270,17 +270,17 @@ export type DetoxLaunchRuntime = {
|
|
|
270
270
|
|
|
271
271
|
export type LaunchAppRuntime = DetoxLaunchRuntime;
|
|
272
272
|
|
|
273
|
-
export type
|
|
273
|
+
export type LaunchAppExitCallback = (logs: string[]) => Promise<void>;
|
|
274
274
|
|
|
275
|
-
type
|
|
275
|
+
type LaunchAppExitOptions = {
|
|
276
276
|
/**
|
|
277
277
|
* Called when the launched app exits. The logs array contains one entry per
|
|
278
278
|
* stdout/stderr line produced by that execution, as reported by limulator.
|
|
279
279
|
*/
|
|
280
|
-
|
|
280
|
+
onExit?: LaunchAppExitCallback;
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
-
type StandardLaunchAppOptions =
|
|
283
|
+
type StandardLaunchAppOptions = LaunchAppExitOptions & {
|
|
284
284
|
/**
|
|
285
285
|
* Launch behavior when the app may already be running.
|
|
286
286
|
* Defaults to `ForegroundIfRunning` server-side.
|
|
@@ -289,7 +289,7 @@ type StandardLaunchAppOptions = LaunchAppShutdownOptions & {
|
|
|
289
289
|
runtime?: undefined;
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
-
type RuntimeLaunchAppOptions =
|
|
292
|
+
type RuntimeLaunchAppOptions = LaunchAppExitOptions & {
|
|
293
293
|
/** Runtime launches must relaunch so runtime injection is applied. */
|
|
294
294
|
mode?: Extract<LaunchAppMode, 'RelaunchIfRunning'>;
|
|
295
295
|
/** Optional app runtime to attach during launch. */
|
|
@@ -970,7 +970,7 @@ type ServerResponse = {
|
|
|
970
970
|
exitCode?: number;
|
|
971
971
|
// Log tail fields
|
|
972
972
|
logs?: string;
|
|
973
|
-
// App
|
|
973
|
+
// App exit fields
|
|
974
974
|
execId?: string;
|
|
975
975
|
logLineCount?: number;
|
|
976
976
|
// App log streaming fields
|
|
@@ -1546,33 +1546,31 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1546
1546
|
return logs.split(/\r?\n/);
|
|
1547
1547
|
};
|
|
1548
1548
|
|
|
1549
|
-
const
|
|
1550
|
-
if (message.type !== '
|
|
1549
|
+
const handleAppExit = (message: ServerResponse): boolean => {
|
|
1550
|
+
if (message.type !== 'appExit') {
|
|
1551
1551
|
return false;
|
|
1552
1552
|
}
|
|
1553
1553
|
|
|
1554
|
-
const { execId, bundleId,
|
|
1554
|
+
const { execId, bundleId, logLineCount } = message;
|
|
1555
1555
|
if (
|
|
1556
1556
|
typeof execId !== 'string' ||
|
|
1557
1557
|
typeof bundleId !== 'string' ||
|
|
1558
|
-
typeof exitCode !== 'number' ||
|
|
1559
|
-
!Number.isInteger(exitCode) ||
|
|
1560
1558
|
typeof logLineCount !== 'number' ||
|
|
1561
1559
|
!Number.isInteger(logLineCount) ||
|
|
1562
1560
|
logLineCount < 0
|
|
1563
1561
|
) {
|
|
1564
|
-
logger.warn('Received malformed
|
|
1562
|
+
logger.warn('Received malformed appExit message:', message);
|
|
1565
1563
|
return true;
|
|
1566
1564
|
}
|
|
1567
1565
|
|
|
1568
|
-
const notification = takeServerNotification('
|
|
1566
|
+
const notification = takeServerNotification('appExit', execId);
|
|
1569
1567
|
if (!notification) {
|
|
1570
|
-
logger.debug(`Received
|
|
1568
|
+
logger.debug(`Received appExit for unknown or already handled execId: ${execId}`);
|
|
1571
1569
|
return true;
|
|
1572
1570
|
}
|
|
1573
1571
|
|
|
1574
1572
|
void notification(message).catch((error) => {
|
|
1575
|
-
logger.error(`Error processing
|
|
1573
|
+
logger.error(`Error processing appExit notification for execId ${execId}:`, error);
|
|
1576
1574
|
});
|
|
1577
1575
|
|
|
1578
1576
|
return true;
|
|
@@ -1652,7 +1650,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1652
1650
|
return;
|
|
1653
1651
|
}
|
|
1654
1652
|
|
|
1655
|
-
if (
|
|
1653
|
+
if (handleAppExit(message)) {
|
|
1656
1654
|
return;
|
|
1657
1655
|
}
|
|
1658
1656
|
|
|
@@ -1912,20 +1910,18 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1912
1910
|
);
|
|
1913
1911
|
}
|
|
1914
1912
|
const mode = launchOptions.runtime ? 'RelaunchIfRunning' : launchOptions.mode;
|
|
1915
|
-
const
|
|
1916
|
-
const execId =
|
|
1917
|
-
if (execId &&
|
|
1918
|
-
registerServerNotification('
|
|
1919
|
-
const { bundleId,
|
|
1913
|
+
const onExit = launchOptions.onExit;
|
|
1914
|
+
const execId = onExit ? generateId() : undefined;
|
|
1915
|
+
if (execId && onExit) {
|
|
1916
|
+
registerServerNotification('appExit', execId, async (message) => {
|
|
1917
|
+
const { bundleId, logLineCount } = message;
|
|
1920
1918
|
if (
|
|
1921
1919
|
typeof bundleId !== 'string' ||
|
|
1922
|
-
typeof exitCode !== 'number' ||
|
|
1923
|
-
!Number.isInteger(exitCode) ||
|
|
1924
1920
|
typeof logLineCount !== 'number' ||
|
|
1925
1921
|
!Number.isInteger(logLineCount) ||
|
|
1926
1922
|
logLineCount < 0
|
|
1927
1923
|
) {
|
|
1928
|
-
logger.warn(`Received malformed
|
|
1924
|
+
logger.warn(`Received malformed appExit payload for execId ${execId}:`, message);
|
|
1929
1925
|
return;
|
|
1930
1926
|
}
|
|
1931
1927
|
|
|
@@ -1935,13 +1931,13 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1935
1931
|
await sendRequest<string>('appLogTail', { bundleId, lines: logLineCount }),
|
|
1936
1932
|
);
|
|
1937
1933
|
} catch (error) {
|
|
1938
|
-
logger.error(`Failed to fetch app logs for
|
|
1934
|
+
logger.error(`Failed to fetch app logs for exit execId ${execId}:`, error);
|
|
1939
1935
|
}
|
|
1940
1936
|
|
|
1941
1937
|
try {
|
|
1942
|
-
await
|
|
1938
|
+
await onExit(logs);
|
|
1943
1939
|
} catch (error) {
|
|
1944
|
-
logger.error(`Error in
|
|
1940
|
+
logger.error(`Error in onExit callback for execId ${execId}:`, error);
|
|
1945
1941
|
}
|
|
1946
1942
|
});
|
|
1947
1943
|
}
|
|
@@ -1952,7 +1948,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
1952
1948
|
execId,
|
|
1953
1949
|
}).catch((error) => {
|
|
1954
1950
|
if (execId) {
|
|
1955
|
-
deleteServerNotification('
|
|
1951
|
+
deleteServerNotification('appExit', execId);
|
|
1956
1952
|
}
|
|
1957
1953
|
throw error;
|
|
1958
1954
|
});
|
|
@@ -45,6 +45,12 @@ export type SyncOptions = {
|
|
|
45
45
|
* Return true to ignore, false to keep.
|
|
46
46
|
*/
|
|
47
47
|
ignore?: (relativePath: string) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Optional predicate for force-including paths that would otherwise be
|
|
50
|
+
* dropped (gitignored generated sources, default Xcode excludes). Same
|
|
51
|
+
* calling convention as `ignore`; return true to force-sync the path.
|
|
52
|
+
*/
|
|
53
|
+
include?: (relativePath: string) => boolean;
|
|
48
54
|
/**
|
|
49
55
|
* Extra files to sync on every sync pass.
|
|
50
56
|
*/
|
|
@@ -596,40 +602,18 @@ export class XcodeInstances extends GeneratedXcodeInstances {
|
|
|
596
602
|
ignoreFn: await createIgnoreFn(localCodePath, {
|
|
597
603
|
basisCacheDir,
|
|
598
604
|
log,
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
relativePath.startsWith('.build/') ||
|
|
603
|
-
relativePath.startsWith('DerivedData/') ||
|
|
604
|
-
relativePath.startsWith('Index.noindex/') ||
|
|
605
|
-
relativePath.startsWith('ModuleCache.noindex/') ||
|
|
606
|
-
relativePath.startsWith('.index-build/')
|
|
607
|
-
) {
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
if (
|
|
611
|
-
relativePath.startsWith('.swiftpm/') ||
|
|
612
|
-
relativePath.startsWith('Pods/') ||
|
|
613
|
-
relativePath.startsWith('Carthage/Build/')
|
|
614
|
-
) {
|
|
615
|
-
return true;
|
|
616
|
-
}
|
|
617
|
-
if (relativePath.includes('/xcuserdata/')) {
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
|
-
if (relativePath.includes('.dSYM/')) {
|
|
621
|
-
return true;
|
|
622
|
-
}
|
|
623
|
-
if (opts?.ignore?.(relativePath)) {
|
|
624
|
-
return true;
|
|
625
|
-
}
|
|
626
|
-
return false;
|
|
627
|
-
},
|
|
605
|
+
xcodeDefaults: true,
|
|
606
|
+
...(opts?.include ? { include: opts.include } : {}),
|
|
607
|
+
...(opts?.ignore ? { additional: opts.ignore } : {}),
|
|
628
608
|
}),
|
|
629
609
|
basisCacheDir,
|
|
630
610
|
watch: opts?.watch ?? true,
|
|
631
611
|
launchMode: 'ForegroundIfRunning',
|
|
632
612
|
log,
|
|
613
|
+
// The limbuild workspace sync understands symlink entries; the
|
|
614
|
+
// limulator app-install sync (ios-client.ts) does not and keeps
|
|
615
|
+
// the default skip behavior.
|
|
616
|
+
syncSymlinks: true,
|
|
633
617
|
...(additionalFiles ? { additionalFiles } : {}),
|
|
634
618
|
};
|
|
635
619
|
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.39.
|
|
1
|
+
export const VERSION = '0.39.5'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.39.
|
|
1
|
+
export declare const VERSION = "0.39.5";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.39.
|
|
1
|
+
export declare const VERSION = "0.39.5";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.39.
|
|
1
|
+
export const VERSION = '0.39.5'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|