Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/stim-cli/src/__tests__/worktree-warm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,27 @@ test('warm copies literal ignored filenames and skips excluded derived data', ()
expect(existsSync(join(target, 'node_modules/pkg/.DerivedData'))).toBe(false);
});

test.each(['android/build/', 'apps/mobile/'])('warm excludes generated autolinking from ignored %s', (ignored) => {
write(root, '.gitignore', `${ignored}\n`);
const app = ignored.startsWith('apps/') ? 'apps/mobile/' : '';
const generated = `${app}android/build/generated`;
write(root, `${generated}/autolinking/autolinking.json`, 'source checkout paths');
write(root, `${generated}/autolinking/package.json.sha`, 'unchanged checksum');
write(root, `${generated}/other/output`, 'reusable output');
write(root, `${generated}/autolinking-extra/keep`, 'unrelated output');
const result = warm();
expect(result.failed).toEqual([]);
expect(existsSync(join(target, generated, 'autolinking'))).toBe(false);
expect(readFileSync(join(target, generated, 'other/output'), 'utf8')).toBe('reusable output');
expect(readFileSync(join(target, generated, 'autolinking-extra/keep'), 'utf8')).toBe('unrelated output');
expect(readFileSync(join(root, generated, 'autolinking/autolinking.json'), 'utf8')).toBe('source checkout paths');
write(target, `${generated}/autolinking/autolinking.json`, 'destination checkout paths');
expect(warm().failed).toEqual([]);
expect(readFileSync(join(target, generated, 'autolinking/autolinking.json'), 'utf8')).toBe(
'destination checkout paths',
);
});

test('exclusive directory publication preserves an empty directory created after the final precheck', () => {
write(root, 'node_modules/pkg/index.js', 'source package');
publication.beforeMkdir = (path) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/stim-cli/src/__tests__/worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ test('H1: cloneIgnoredEntries carries a top-level ignored $(...) filename as a l

test('isCarrySkipped skips .DerivedData at any depth and treats .stim normally', () => {
for (const rel of [
'android/build/generated/autolinking',
'apps/mobile/android/build/generated/autolinking/autolinking.json',
'.DerivedData',
'ios/build/.DerivedData',
'node_modules/expo-modules-jsi/apple/.DerivedData',
Expand Down
4 changes: 3 additions & 1 deletion packages/stim-cli/src/guide/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,9 @@ OPT-IN CONCURRENCY LIMITS (UNLIMITED BY DEFAULT)
paths eligible under the main checkout's Git ignore rules, including .env
and local configuration. The source's nonempty
.worktreeexclude replaces its resolved worktree.exclude setting. Nested
registered worktrees and .DerivedData are excluded. Warm also skips paths
registered worktrees, .DerivedData, and android/build/generated/autolinking
caches are excluded, including in nested apps. Gradle regenerates autolinking
for the destination checkout on its next build. Warm also skips paths
overlapping a nested destination worktree or below a symlink ancestor.

Large copies stage privately outside Git working trees on the destination
Expand Down
16 changes: 10 additions & 6 deletions packages/stim-cli/src/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import { makeTemporaryDirectory, removeTemporaryEntry } from './temporary.ts';
const CARRY_SKIP_BASENAMES = new Set(['.DerivedData']);

export function isCarrySkipped(rel: string): boolean {
return String(rel)
.split('/')
.some((seg) => CARRY_SKIP_BASENAMES.has(seg));
return (
/(^|\/)android\/build\/generated\/autolinking(\/|$)/.test(rel) ||
String(rel)
.split('/')
.some((seg) => CARRY_SKIP_BASENAMES.has(seg))
);
}

export function gitCommonDir(cwd: string): string | null {
Expand Down Expand Up @@ -226,7 +229,7 @@ function missingDestinationReason(target: string, rel: string): string | null {
return lstatSync(join(target, rel), { throwIfNoEntry: false }) ? 'exists' : null;
}

function publishMissingEntry(from: string, to: string): boolean {
function publishMissingEntry(from: string, to: string, rel: string): boolean {
let cloned = true;
const stat = lstatSync(from);
if (stat.isSymbolicLink()) {
Expand All @@ -248,7 +251,8 @@ function publishMissingEntry(from: string, to: string): boolean {
} else if (stat.isDirectory()) {
mkdirSync(to, { mode: stat.mode | 0o700 });
for (const name of readdirSync(from)) {
if (!CARRY_SKIP_BASENAMES.has(name) && !publishMissingEntry(join(from, name), join(to, name))) {
const child = `${rel}/${name}`;
if (!isCarrySkipped(child) && !publishMissingEntry(join(from, name), join(to, name), child)) {
cloned = false;
}
}
Expand Down Expand Up @@ -305,7 +309,7 @@ export function cloneIgnoredEntries({
continue;
}
mkdirSync(dirname(to), { recursive: true });
if (!publishMissingEntry(destination, to)) cloned = false;
if (!publishMissingEntry(destination, to, rel)) cloned = false;
copied.push(rel);
} catch (e) {
failed.push({ file: rel, error: String((e as Error)?.message || e) });
Expand Down
Loading