Skip to content

Commit

Permalink
feat: add --offline mode (#879)
Browse files Browse the repository at this point in the history
## PR Checklist

- [x] Addresses an existing open issue: fixes #878
- [x] That issue was marked as [`status: accepting
prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in
[CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md)
were taken

## Overview

Adds `--offline` flag that skips running scripts. That includes a
timeout for `npm whoami`, as I've found that command to hang when I'm on
an airplane.

Also corrects a bit of the logic around adding the user as a
contributor, while I'm in the area. And fixes up unit tests around that.
  • Loading branch information
JoshuaKGoldberg committed Sep 29, 2023
1 parent 7623d6a commit 715e79d
Show file tree
Hide file tree
Showing 20 changed files with 295 additions and 72 deletions.
17 changes: 16 additions & 1 deletion docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ npx create-typescript-app --exclude-lint-package-json --exclude-lint-packages --
### Skipping API Calls

> Alternately, see [Offline Mode](#offline-mode) to skip API calls without disabling features
You can prevent the migration script from making some network-based changes using any or all of the following CLI flags:

- `--skip-all-contributors-api` _(`boolean`)_: Skips network calls that fetch all-contributors data from GitHub
- This flag does nothing if `--skip-all-contributors` was specified.
- This flag does nothing if `--exclude-all-contributors` was specified.
- `--skip-github-api` _(`boolean`)_: Skips calling to GitHub APIs.
- `--skip-install` _(`boolean`)_: Skips installing all the new template packages with `pnpm`.

Expand All @@ -133,3 +135,16 @@ For example, providing all local change skip flags:
```shell
npx create-typescript-app --skip-removal --skip-restore --skip-uninstall
```

## Offline Mode

You can run `create-typescript-app` in an "offline" mode with `--offline`.
Doing so will:

- Enable `--exclude-all-contributors-api` and `--skip-github-api`
- Skip network calls when setting up contributors
- Run pnpm commands with pnpm's `--offline` mode

```shell
npx create-typescript-app --offline
```
2 changes: 1 addition & 1 deletion src/create/createWithOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function createWithOptions({ github, options }: GitHubAndOptions) {

if (!options.excludeAllContributors && !options.skipAllContributorsApi) {
await withSpinner("Adding contributors to table", async () => {
await addToolAllContributors(options.owner);
await addToolAllContributors(options);
});
}

Expand Down
7 changes: 3 additions & 4 deletions src/initialize/initializeWithOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function initializeWithOptions({

if (!options.excludeAllContributors) {
await withSpinner("Updating existing contributor details", async () => {
await addOwnerAsAllContributor(options.owner);
await addOwnerAsAllContributor(options);
});
}

Expand All @@ -50,9 +50,8 @@ export async function initializeWithOptions({
}

if (!options.skipUninstall) {
await withSpinner(
"Uninstalling initialization-only packages",
uninstallPackages,
await withSpinner("Uninstalling initialization-only packages", async () =>
uninstallPackages(options.offline),
);
}

Expand Down
98 changes: 98 additions & 0 deletions src/shared/getGitHubUserAsAllContributor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import chalk from "chalk";
import { SpyInstance, beforeEach, describe, expect, it, vi } from "vitest";

import { getGitHubUserAsAllContributor } from "./getGitHubUserAsAllContributor.js";

const mock$ = vi.fn();

vi.mock("execa", () => ({
get $() {
return mock$;
},
}));

let mockConsoleWarn: SpyInstance;

const owner = "TestOwner";

describe("getGitHubUserAsAllContributor", () => {
beforeEach(() => {
mockConsoleWarn = vi
.spyOn(console, "warn")
.mockImplementation(() => undefined);
});

it("defaults to owner with a log when options.offline is true", async () => {
const actual = await getGitHubUserAsAllContributor({
offline: true,
owner,
});

expect(actual).toEqual(owner);
expect(mockConsoleWarn).toHaveBeenCalledWith(
chalk.gray(
`Skipping populating all-contributors contributions for TestOwner because in --offline mode.`,
),
);
});

it("uses the user from gh api user when it succeeds", async () => {
const login = "gh-api-user";

mock$.mockResolvedValueOnce({
stdout: JSON.stringify({ login }),
});

await getGitHubUserAsAllContributor({ owner });

expect(mockConsoleWarn).not.toHaveBeenCalled();
expect(mock$.mock.calls).toMatchInlineSnapshot(`
[
[
[
"gh api user",
],
],
[
[
"npx -y all-contributors-cli@6.25 add ",
" ",
"",
],
"gh-api-user",
"code,content,doc,ideas,infra,maintenance,projectManagement,tool",
],
]
`);
});

it("defaults the user to the owner when gh api user fails", async () => {
mock$.mockRejectedValueOnce({});

await getGitHubUserAsAllContributor({ owner });

expect(mockConsoleWarn).toHaveBeenCalledWith(
chalk.gray(
`Couldn't authenticate GitHub user, falling back to the provided owner name '${owner}'.`,
),
);
expect(mock$.mock.calls).toMatchInlineSnapshot(`
[
[
[
"gh api user",
],
],
[
[
"npx -y all-contributors-cli@6.25 add ",
" ",
"",
],
"TestOwner",
"code,content,doc,ideas,infra,maintenance,projectManagement,tool",
],
]
`);
});
});
20 changes: 17 additions & 3 deletions src/shared/getGitHubUserAsAllContributor.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import chalk from "chalk";
import { $ } from "execa";

import { Options } from "./types.js";

interface GhUserOutput {
login: string;
}

export async function getGitHubUserAsAllContributor(owner: string) {
export async function getGitHubUserAsAllContributor(
options: Pick<Options, "offline" | "owner">,
) {
if (options.offline) {
console.warn(
chalk.gray(
`Skipping populating all-contributors contributions for ${options.owner} because in --offline mode.`,
),
);
return options.owner;
}

let user: string;

try {
user = (JSON.parse((await $`gh api user`).stdout) as GhUserOutput).login;
} catch {
console.warn(
chalk.gray(
`Couldn't authenticate GitHub user, falling back to the provided owner name '${owner}'.`,
`Couldn't authenticate GitHub user, falling back to the provided owner name '${options.owner}'.`,
),
);
user = owner;
user = options.owner;
}

const contributions = [
Expand Down
1 change: 1 addition & 0 deletions src/shared/options/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const allArgOptions = {
logo: { type: "string" },
"logo-alt": { type: "string" },
mode: { type: "string" },
offline: { type: "boolean" },
owner: { type: "string" },
repository: { type: "string" },
"skip-all-contributors-api": { type: "boolean" },
Expand Down
1 change: 1 addition & 0 deletions src/shared/options/augmentOptionsWithExcludes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const optionsBase = {
funding: undefined,
logo: undefined,
mode: "create",
offline: true,
owner: "",
repository: "",
skipGitHubApi: false,
Expand Down
1 change: 1 addition & 0 deletions src/shared/options/optionsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const optionsSchemaShape = {
mode: z
.union([z.literal("create"), z.literal("initialize"), z.literal("migrate")])
.optional(),
offline: z.boolean().optional(),
owner: z.string().optional(),
repository: z.string().optional(),
skipAllContributorsApi: z.boolean().optional(),
Expand Down
40 changes: 39 additions & 1 deletion src/shared/options/readOptions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import z from "zod";

import { Options } from "../types.js";
import { optionsSchemaShape } from "./optionsSchema.js";
import { readOptions } from "./readOptions.js";

Expand Down Expand Up @@ -30,6 +31,7 @@ const emptyOptions = {
excludeRenovate: undefined,
excludeTests: undefined,
funding: undefined,
offline: undefined,
owner: undefined,
repository: undefined,
skipAllContributorsApi: undefined,
Expand Down Expand Up @@ -59,7 +61,6 @@ vi.mock("./augmentOptionsWithExcludes.js", () => ({
get augmentOptionsWithExcludes() {
return mockAugmentOptionsWithExcludes;
},
// return { ...emptyOptions, ...mockOptions };
}));

const mockDetectEmailRedundancy = vi.fn();
Expand Down Expand Up @@ -317,4 +318,41 @@ describe("readOptions", () => {
},
});
});

it("skips API calls when --offline is true", async () => {
mockAugmentOptionsWithExcludes.mockImplementation((options: Options) => ({
...emptyOptions,
...mockOptions,
...options,
}));
mockGetPrefillOrPromptedOption.mockImplementation(() => "mock");
mockEnsureRepositoryExists.mockResolvedValue({
github: mockOptions.github,
repository: mockOptions.repository,
});

expect(
await readOptions(["--base", mockOptions.base, "--offline"], "create"),
).toStrictEqual({
cancelled: false,
github: mockOptions.github,
options: {
...emptyOptions,
...mockOptions,
access: "public",
description: "mock",
email: {
github: "mock",
npm: "mock",
},
logo: undefined,
mode: "create",
offline: true,
owner: "mock",
skipAllContributorsApi: true,
skipGitHubApi: true,
title: "mock",
},
});
});
});
8 changes: 5 additions & 3 deletions src/shared/options/readOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ export async function readOptions(
excludeRenovate: values["exclude-renovate"],
excludeTests: values["unit-tests"],
funding: values.funding,
offline: values.offline,
owner: values.owner,
repository: values.repository,
skipAllContributorsApi: values["skip-all-contributors-api"],
skipGitHubApi: values["skip-github-api"],
skipAllContributorsApi:
values["skip-all-contributors-api"] ?? values.offline,
skipGitHubApi: values["skip-github-api"] ?? values.offline,
skipInstall: values["skip-install"],
skipRemoval: values["skip-removal"],
skipRestore: values["skip-restore"],
Expand Down Expand Up @@ -132,7 +134,7 @@ export async function readOptions(
}

const { github, repository } = await ensureRepositoryExists(
values["skip-github-api"]
options.skipGitHubApi
? undefined
: await withSpinner("Checking GitHub authentication", getGitHub),
{
Expand Down
1 change: 1 addition & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface Options {
funding?: string;
logo: OptionsLogo | undefined;
mode: Mode;
offline?: boolean;
owner: string;
repository: string;
skipAllContributorsApi?: boolean;
Expand Down

0 comments on commit 715e79d

Please sign in to comment.