Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"categories": {
"correctness": "error",
"suspicious": "warn",
"pedantic": "off",
"pedantic": "warn",
"perf": "warn",
"restriction": "off",
"style": "off"
Expand Down
4 changes: 2 additions & 2 deletions perf/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ try {
const samplesMs: number[] = [];

try {
for (let _ = 0; _ < config.warmups; _ += 1) {
for (let warmupIndex = 0; warmupIndex < config.warmups; warmupIndex += 1) {
await runtime.clone();
}

for (let _ = 0; _ < config.iterations; _ += 1) {
for (let iterationIndex = 0; iterationIndex < config.iterations; iterationIndex += 1) {
const startedAt = performance.now();
await runtime.clone();
samplesMs.push(performance.now() - startedAt);
Expand Down
6 changes: 3 additions & 3 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import degit from './index.js';
import { base, tryRequire } from './utils.js';

const dirname = path.dirname(fileURLToPath(import.meta.url));

Check warning on line 12 in src/bin.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint-plugin-unicorn(prefer-import-meta-properties)

Do not construct dirname.

type Choice = {
message: string;
Expand Down Expand Up @@ -94,11 +94,11 @@
};

return glob('**/map.json', { cwd: base })
.flatMap(getChoices)

Check warning on line 97 in src/bin.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint-plugin-unicorn(no-array-callback-reference)

Avoid passing a function reference directly to iterator methods
.toSorted((a, b) => (accessLookup.get(b.value) || 0) - (accessLookup.get(a.value) || 0));
}

async function promptForSource(): Promise<PromptResult> {

Check warning on line 101 in src/bin.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint(require-await)

Async function has no `await` expression.
const sourcePrompt = {
choices: getInteractiveChoices(),
message: 'Repo to clone?',
Expand All @@ -106,7 +106,7 @@
suggest: (input: string, promptChoices: Choice[]) =>
promptChoices.filter(({ value }) => fuzzysearch(input, value)),
type: 'autocomplete',
} as any;
};

return enquirer.prompt<PromptResult>([
sourcePrompt,
Expand All @@ -121,7 +121,7 @@
name: 'cache',
type: 'toggle',
},
] as any);
]);
}

async function confirmOverwrite(): Promise<boolean> {
Expand All @@ -131,7 +131,7 @@
name: 'force',
type: 'toggle',
},
] as any);
]);

return force;
}
Expand All @@ -152,7 +152,7 @@
const empty = !fs.existsSync(options.dest) || fs.readdirSync(options.dest).length === 0;

if (!empty) {
if (!(await confirmOverwrite())) {

Check warning on line 155 in src/bin.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint-plugin-unicorn(no-lonely-if)

Unexpected `if` as the only statement in a `if` block without `else`.
console.error(colors.magenta('! Directory not empty — aborting'));
return;
}
Expand Down Expand Up @@ -221,8 +221,8 @@
}

if (!process.env.VITEST) {
main(process.argv).catch((error) => {
console.error(error);
process.exit(1);
});

Check warning on line 227 in src/bin.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint-plugin-unicorn(prefer-top-level-await)

Prefer top-level await over using a promise chain.
}
2 changes: 1 addition & 1 deletion src/git-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
};
}

function mapServerRef(serverRef: any): Ref | undefined {
function mapServerRef(serverRef: { hash?: string; name?: string; oid?: string; ref?: string }): Ref | undefined {
const refName = String(serverRef.ref || serverRef.name || '');
const refHash = String(serverRef.oid || serverRef.hash || '');

Expand Down Expand Up @@ -214,7 +214,7 @@
};
}

async function fetchRefsWithGitCli(repo: Repo) {

Check warning on line 217 in src/git-client.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint(require-await)

Async function has no `await` expression.
return new Promise<Ref[]>((resolve, reject) => {
const child = spawn('git', ['ls-remote', '--symref', getGitUrl(repo)], {
stdio: ['ignore', 'pipe', 'pipe'],
Expand Down Expand Up @@ -258,7 +258,7 @@
const url = getGitUrl(repo);
const normalizeRefs = (
refs: Array<{ hash?: string; oid?: string; ref?: string; name?: string }>,
) => dedupeRefs(refs.map(mapServerRef).filter((ref): ref is Ref => Boolean(ref)));

Check warning on line 261 in src/git-client.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint-plugin-unicorn(prefer-native-coercion-functions)

The function is equivalent to `Boolean`. Call `Boolean` directly.

Check warning on line 261 in src/git-client.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint-plugin-unicorn(no-array-callback-reference)

Avoid passing a function reference directly to iterator methods

try {
const refs = await git.listServerRefs({
Expand Down Expand Up @@ -415,4 +415,4 @@
};
}

export const defaultGitClient = createGitClient();

Check warning on line 418 in src/git-client.ts

View workflow job for this annotation

GitHub Actions / Quality

eslint(max-lines)

File has too many lines (418).
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function fetch(url: string, dest: string, proxy?: string): Promise<void>
if (proxy) {
const parsedUrl = URL.parse(url);
options = {
agent: Agent(proxy) as unknown as import('node:http').Agent,
agent: Agent(proxy) as import('node:http').Agent,
hostname: parsedUrl.host,
path: parsedUrl.path,
};
Expand Down
23 changes: 9 additions & 14 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@ export function compareDirToExpected(dir, files) {

export function createMockGit(stubs = {}) {
const calls = [];
const resolveStub = async (call, ...args) => {
const resolveStub = (call, ...args) => {
if (!Object.hasOwn(stubs, call)) {
return Promise.reject(new Error(`Unexpected git call: ${call}`));
throw new Error(`Unexpected git call: ${call}`);
}

const stub = stubs[call];
if (typeof stub === 'function') {
return stub(...args);
}

return Promise.resolve(stub);
return stub;
};

const fetchRefs = async (repo) => {
const fetchRefs = (repo) => {
const call = `fetchRefs ${getGitUrl(repo)}`;
calls.push(call);
return resolveStub(call, repo);
};

const clone = async (repo, dest, ref, transport) => {
const clone = (repo, dest, ref, transport) => {
const call = `clone ${getGitUrl(repo, transport)} ${dest}${ref ? ` ${ref}` : ''}`;
calls.push(call);
return resolveStub(call, repo, dest, ref, transport);
Expand All @@ -61,23 +61,21 @@ export function createMockFetch(steps) {

const response = steps[step++];
if (!response) {
return Promise.reject(new Error('No mock fetch step configured'));
throw new Error('No mock fetch step configured');
}

if (response.status >= 300 && response.status < 400) {
return fn(response.location, file, proxy);
}

if (response.status >= 400) {
return Promise.reject(
throw (
response.error || {
code: response.code || response.status,
message: response.message || 'mock fetch error',
},
}
);
}

return Promise.resolve();
};

return { calls, fn };
Expand All @@ -90,11 +88,10 @@ export function createCopyFetch(sourceFile) {
const copyWithRetry = (destination, attempt = 1) => {
try {
fs.copyFileSync(sourceFile, destination);
return Promise.resolve();
} catch (error) {
const isRetryable = error?.code === 'EPERM' || error?.code === 'EBUSY';
if (!isRetryable || attempt === maxAttempts) {
return Promise.reject(error);
throw error;
}

return delay(25 * attempt).then(() => copyWithRetry(destination, attempt + 1));
Expand All @@ -104,8 +101,6 @@ export function createCopyFetch(sourceFile) {
const fn = async (url, file, proxy) => {
calls.push({ file, proxy, url });
await copyWithRetry(file);

return Promise.resolve();
};

return { calls, fn };
Expand Down
2 changes: 1 addition & 1 deletion test/integration/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function createReleaseRunner() {
const { command, args } = getReleaseCommand();

return {
async clone(source: string, dest: string) {
clone(source: string, dest: string) {
const result = child_process.spawnSync(command, [...args, source, dest], {
encoding: 'utf8',
env: process.env,
Expand Down
106 changes: 57 additions & 49 deletions test/unit/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ import enquirer from 'enquirer';
const mockDegit = vi.mocked(degit);
const mockPrompt = vi.mocked(enquirer.prompt);

type MockDegitInstance = ReturnType<typeof degit>;

function createMockDegit(
cloneImpl: () => Promise<void>,
handlers?: Record<string, (event: { message: string }) => void>,
): MockDegitInstance {
const instance = {
clone: vi.fn(cloneImpl),
on(event: string, handler: (event: { message: string }) => void) {
if (handlers) {
handlers[event] = handler;
}

return instance;
},
};

return instance as MockDegitInstance;
}

async function waitForCondition(fn, timeoutMs = 3000, startedAt = Date.now()) {
if (fn()) {
return;
Expand All @@ -54,22 +74,20 @@ async function waitForCondition(fn, timeoutMs = 3000, startedAt = Date.now()) {
assert.fail('timeout waiting for condition');
}

await new Promise((r) => setTimeout(r, 5));
await new Promise<void>((resolve) => {
setTimeout(resolve, 5);
});
return waitForCondition(fn, timeoutMs, startedAt);
}

function mockEventClone(eventName, message) {
const handlers = {};
mockDegit.mockReturnValue({
clone: vi.fn().mockImplementation(() => {
handlers[eventName]({ message });
const handlers: Record<string, (event: { message: string }) => void> = {};
mockDegit.mockReturnValue(
createMockDegit(() => {
handlers[eventName]?.({ message });
return Promise.resolve();
}),
on: vi.fn(function on(ev, fn) {
handlers[ev] = fn;
return this;
}),
} as never);
}, handlers),
);
return handlers;
}

Expand All @@ -78,12 +96,9 @@ async function withCloneFailure(
error: Error,
assertions: (exitSpy: ReturnType<typeof vi.spyOn>, errSpy: ReturnType<typeof vi.spyOn>) => void,
) {
mockDegit.mockReturnValue({
clone: vi.fn().mockReturnValue(Promise.reject(error)),
on: vi.fn().mockReturnThis(),
} as never);
mockDegit.mockReturnValue(createMockDegit(() => Promise.reject(error)));

const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as never);
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

try {
Expand All @@ -97,7 +112,6 @@ async function withCloneFailure(
}
}

describe('degit bin', () => {
const binTmp = '.tmp/bin-suite';
const repoRoot = process.cwd();
const rootBin = path.join(repoRoot, 'degit');
Expand All @@ -107,34 +121,32 @@ describe('degit bin', () => {
fs.rmSync(interactiveBase, { force: true, recursive: true });
}

beforeEach(async () => {
await rimraf(binTmp);
clearInteractiveFixtures();
vi.clearAllMocks();
mockDegit.mockReturnValue({
clone: vi.fn().mockResolvedValue(undefined),
on: vi.fn().mockReturnThis(),
} as never);
});
afterEach(async () => {
await rimraf(binTmp);
clearInteractiveFixtures();
});
beforeEach(async () => {
await rimraf(binTmp);
clearInteractiveFixtures();
vi.clearAllMocks();
mockDegit.mockReturnValue(createMockDegit(() => Promise.resolve()));
});

it('runs the built root bin when --help is executed', () => {
const result = child_process.spawnSync('node', [rootBin, '--help'], {
env: {
...process.env,
VITEST: '',
},
encoding: 'utf8',
});
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
assert.ok(output.length > 0);
assert.ok(output.includes('degit'));
afterEach(async () => {
await rimraf(binTmp);
clearInteractiveFixtures();
});

it('runs the built root bin when --help is executed', () => {
const result = child_process.spawnSync('node', [rootBin, '--help'], {
env: {
...process.env,
VITEST: '',
},
encoding: 'utf8',
});
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
assert.ok(output.length > 0);
assert.ok(output.includes('degit'));
});

it('writes help to stdout when argv includes --help', async () => {
it('writes help to stdout when argv includes --help', async () => {
const chunks: string[] = [];
const orig = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk, enc, cb) => {
Expand All @@ -158,7 +170,7 @@ describe('degit bin', () => {
await main(['node', 'bin', 'user/repo', 'out', '-f']);
assert.equal(mockDegit.mock.calls.length, 1);
assert.equal(mockDegit.mock.calls[0][0], 'user/repo');
assert.equal((mockDegit.mock.calls[0][1] as any).force, true);
assert.equal(mockDegit.mock.calls[0][1]?.force, true);
const instance = mockDegit.mock.results[0].value;
assert.equal(instance.clone.mock.calls[0][0], 'out');
});
Expand All @@ -180,7 +192,7 @@ describe('degit bin', () => {
JSON.stringify({ main: '2026-01-01T00:00:00.000Z' }),
);

mockPrompt.mockImplementation(async (questions) => {
mockPrompt.mockImplementation((questions) => {
const srcQuestion = (
questions as Array<{ name?: string; choices?: Array<{ value: string }> }>
).find((question) => question.name === 'src');
Expand Down Expand Up @@ -269,10 +281,7 @@ describe('degit bin', () => {
});

it('forwards explicit git mode when argv passes --mode=git', async () => {
mockDegit.mockReturnValue({
clone: vi.fn().mockResolvedValue(undefined),
on: vi.fn().mockReturnThis(),
} as never);
mockDegit.mockReturnValue(createMockDegit(() => Promise.resolve()));
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
await main(['node', 'bin', 'a/b', 'dest', '--mode=git']);
Expand All @@ -284,4 +293,3 @@ describe('degit bin', () => {
warnSpy.mockRestore();
}
});
});
Loading
Loading