Skip to content

Commit 715e79d

Browse files
authoredSep 29, 2023
feat: add --offline mode (#879)
## 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.
1 parent 7623d6a commit 715e79d

20 files changed

+295
-72
lines changed
 

Diff for: ‎docs/Options.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ npx create-typescript-app --exclude-lint-package-json --exclude-lint-packages --
105105
106106
### Skipping API Calls
107107

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

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

@@ -133,3 +135,16 @@ For example, providing all local change skip flags:
133135
```shell
134136
npx create-typescript-app --skip-removal --skip-restore --skip-uninstall
135137
```
138+
139+
## Offline Mode
140+
141+
You can run `create-typescript-app` in an "offline" mode with `--offline`.
142+
Doing so will:
143+
144+
- Enable `--exclude-all-contributors-api` and `--skip-github-api`
145+
- Skip network calls when setting up contributors
146+
- Run pnpm commands with pnpm's `--offline` mode
147+
148+
```shell
149+
npx create-typescript-app --offline
150+
```

Diff for: ‎src/create/createWithOptions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export async function createWithOptions({ github, options }: GitHubAndOptions) {
2929

3030
if (!options.excludeAllContributors && !options.skipAllContributorsApi) {
3131
await withSpinner("Adding contributors to table", async () => {
32-
await addToolAllContributors(options.owner);
32+
await addToolAllContributors(options);
3333
});
3434
}
3535

Diff for: ‎src/initialize/initializeWithOptions.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export async function initializeWithOptions({
3535

3636
if (!options.excludeAllContributors) {
3737
await withSpinner("Updating existing contributor details", async () => {
38-
await addOwnerAsAllContributor(options.owner);
38+
await addOwnerAsAllContributor(options);
3939
});
4040
}
4141

@@ -50,9 +50,8 @@ export async function initializeWithOptions({
5050
}
5151

5252
if (!options.skipUninstall) {
53-
await withSpinner(
54-
"Uninstalling initialization-only packages",
55-
uninstallPackages,
53+
await withSpinner("Uninstalling initialization-only packages", async () =>
54+
uninstallPackages(options.offline),
5655
);
5756
}
5857

Diff for: ‎src/shared/getGitHubUserAsAllContributor.test.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import chalk from "chalk";
2+
import { SpyInstance, beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
import { getGitHubUserAsAllContributor } from "./getGitHubUserAsAllContributor.js";
5+
6+
const mock$ = vi.fn();
7+
8+
vi.mock("execa", () => ({
9+
get $() {
10+
return mock$;
11+
},
12+
}));
13+
14+
let mockConsoleWarn: SpyInstance;
15+
16+
const owner = "TestOwner";
17+
18+
describe("getGitHubUserAsAllContributor", () => {
19+
beforeEach(() => {
20+
mockConsoleWarn = vi
21+
.spyOn(console, "warn")
22+
.mockImplementation(() => undefined);
23+
});
24+
25+
it("defaults to owner with a log when options.offline is true", async () => {
26+
const actual = await getGitHubUserAsAllContributor({
27+
offline: true,
28+
owner,
29+
});
30+
31+
expect(actual).toEqual(owner);
32+
expect(mockConsoleWarn).toHaveBeenCalledWith(
33+
chalk.gray(
34+
`Skipping populating all-contributors contributions for TestOwner because in --offline mode.`,
35+
),
36+
);
37+
});
38+
39+
it("uses the user from gh api user when it succeeds", async () => {
40+
const login = "gh-api-user";
41+
42+
mock$.mockResolvedValueOnce({
43+
stdout: JSON.stringify({ login }),
44+
});
45+
46+
await getGitHubUserAsAllContributor({ owner });
47+
48+
expect(mockConsoleWarn).not.toHaveBeenCalled();
49+
expect(mock$.mock.calls).toMatchInlineSnapshot(`
50+
[
51+
[
52+
[
53+
"gh api user",
54+
],
55+
],
56+
[
57+
[
58+
"npx -y all-contributors-cli@6.25 add ",
59+
" ",
60+
"",
61+
],
62+
"gh-api-user",
63+
"code,content,doc,ideas,infra,maintenance,projectManagement,tool",
64+
],
65+
]
66+
`);
67+
});
68+
69+
it("defaults the user to the owner when gh api user fails", async () => {
70+
mock$.mockRejectedValueOnce({});
71+
72+
await getGitHubUserAsAllContributor({ owner });
73+
74+
expect(mockConsoleWarn).toHaveBeenCalledWith(
75+
chalk.gray(
76+
`Couldn't authenticate GitHub user, falling back to the provided owner name '${owner}'.`,
77+
),
78+
);
79+
expect(mock$.mock.calls).toMatchInlineSnapshot(`
80+
[
81+
[
82+
[
83+
"gh api user",
84+
],
85+
],
86+
[
87+
[
88+
"npx -y all-contributors-cli@6.25 add ",
89+
" ",
90+
"",
91+
],
92+
"TestOwner",
93+
"code,content,doc,ideas,infra,maintenance,projectManagement,tool",
94+
],
95+
]
96+
`);
97+
});
98+
});

Diff for: ‎src/shared/getGitHubUserAsAllContributor.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
import chalk from "chalk";
22
import { $ } from "execa";
33

4+
import { Options } from "./types.js";
5+
46
interface GhUserOutput {
57
login: string;
68
}
79

8-
export async function getGitHubUserAsAllContributor(owner: string) {
10+
export async function getGitHubUserAsAllContributor(
11+
options: Pick<Options, "offline" | "owner">,
12+
) {
13+
if (options.offline) {
14+
console.warn(
15+
chalk.gray(
16+
`Skipping populating all-contributors contributions for ${options.owner} because in --offline mode.`,
17+
),
18+
);
19+
return options.owner;
20+
}
21+
922
let user: string;
23+
1024
try {
1125
user = (JSON.parse((await $`gh api user`).stdout) as GhUserOutput).login;
1226
} catch {
1327
console.warn(
1428
chalk.gray(
15-
`Couldn't authenticate GitHub user, falling back to the provided owner name '${owner}'.`,
29+
`Couldn't authenticate GitHub user, falling back to the provided owner name '${options.owner}'.`,
1630
),
1731
);
18-
user = owner;
32+
user = options.owner;
1933
}
2034

2135
const contributions = [

Diff for: ‎src/shared/options/args.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const allArgOptions = {
3030
logo: { type: "string" },
3131
"logo-alt": { type: "string" },
3232
mode: { type: "string" },
33+
offline: { type: "boolean" },
3334
owner: { type: "string" },
3435
repository: { type: "string" },
3536
"skip-all-contributors-api": { type: "boolean" },

Diff for: ‎src/shared/options/augmentOptionsWithExcludes.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const optionsBase = {
3535
funding: undefined,
3636
logo: undefined,
3737
mode: "create",
38+
offline: true,
3839
owner: "",
3940
repository: "",
4041
skipGitHubApi: false,

Diff for: ‎src/shared/options/optionsSchema.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const optionsSchemaShape = {
4444
mode: z
4545
.union([z.literal("create"), z.literal("initialize"), z.literal("migrate")])
4646
.optional(),
47+
offline: z.boolean().optional(),
4748
owner: z.string().optional(),
4849
repository: z.string().optional(),
4950
skipAllContributorsApi: z.boolean().optional(),

Diff for: ‎src/shared/options/readOptions.test.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it, vi } from "vitest";
22
import z from "zod";
33

4+
import { Options } from "../types.js";
45
import { optionsSchemaShape } from "./optionsSchema.js";
56
import { readOptions } from "./readOptions.js";
67

@@ -30,6 +31,7 @@ const emptyOptions = {
3031
excludeRenovate: undefined,
3132
excludeTests: undefined,
3233
funding: undefined,
34+
offline: undefined,
3335
owner: undefined,
3436
repository: undefined,
3537
skipAllContributorsApi: undefined,
@@ -59,7 +61,6 @@ vi.mock("./augmentOptionsWithExcludes.js", () => ({
5961
get augmentOptionsWithExcludes() {
6062
return mockAugmentOptionsWithExcludes;
6163
},
62-
// return { ...emptyOptions, ...mockOptions };
6364
}));
6465

6566
const mockDetectEmailRedundancy = vi.fn();
@@ -317,4 +318,41 @@ describe("readOptions", () => {
317318
},
318319
});
319320
});
321+
322+
it("skips API calls when --offline is true", async () => {
323+
mockAugmentOptionsWithExcludes.mockImplementation((options: Options) => ({
324+
...emptyOptions,
325+
...mockOptions,
326+
...options,
327+
}));
328+
mockGetPrefillOrPromptedOption.mockImplementation(() => "mock");
329+
mockEnsureRepositoryExists.mockResolvedValue({
330+
github: mockOptions.github,
331+
repository: mockOptions.repository,
332+
});
333+
334+
expect(
335+
await readOptions(["--base", mockOptions.base, "--offline"], "create"),
336+
).toStrictEqual({
337+
cancelled: false,
338+
github: mockOptions.github,
339+
options: {
340+
...emptyOptions,
341+
...mockOptions,
342+
access: "public",
343+
description: "mock",
344+
email: {
345+
github: "mock",
346+
npm: "mock",
347+
},
348+
logo: undefined,
349+
mode: "create",
350+
offline: true,
351+
owner: "mock",
352+
skipAllContributorsApi: true,
353+
skipGitHubApi: true,
354+
title: "mock",
355+
},
356+
});
357+
});
320358
});

Diff for: ‎src/shared/options/readOptions.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,12 @@ export async function readOptions(
7575
excludeRenovate: values["exclude-renovate"],
7676
excludeTests: values["unit-tests"],
7777
funding: values.funding,
78+
offline: values.offline,
7879
owner: values.owner,
7980
repository: values.repository,
80-
skipAllContributorsApi: values["skip-all-contributors-api"],
81-
skipGitHubApi: values["skip-github-api"],
81+
skipAllContributorsApi:
82+
values["skip-all-contributors-api"] ?? values.offline,
83+
skipGitHubApi: values["skip-github-api"] ?? values.offline,
8284
skipInstall: values["skip-install"],
8385
skipRemoval: values["skip-removal"],
8486
skipRestore: values["skip-restore"],
@@ -132,7 +134,7 @@ export async function readOptions(
132134
}
133135

134136
const { github, repository } = await ensureRepositoryExists(
135-
values["skip-github-api"]
137+
options.skipGitHubApi
136138
? undefined
137139
: await withSpinner("Checking GitHub authentication", getGitHub),
138140
{

Diff for: ‎src/shared/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface Options {
6262
funding?: string;
6363
logo: OptionsLogo | undefined;
6464
mode: Mode;
65+
offline?: boolean;
6566
owner: string;
6667
repository: string;
6768
skipAllContributorsApi?: boolean;

0 commit comments

Comments
 (0)
Please sign in to comment.